Created a groups page for students and teachers

This commit is contained in:
Tiago Ribeiro
2024-08-17 20:18:28 +01:00
parent f0ff6ac691
commit 229275aaee
16 changed files with 2016 additions and 2222 deletions

View File

@@ -13,6 +13,7 @@ import {
BsCurrencyDollar, BsCurrencyDollar,
BsClipboardData, BsClipboardData,
BsFileLock, BsFileLock,
BsPeople,
} from "react-icons/bs"; } from "react-icons/bs";
import {CiDumbbell} from "react-icons/ci"; import {CiDumbbell} from "react-icons/ci";
import {RiLogoutBoxFill} from "react-icons/ri"; import {RiLogoutBoxFill} from "react-icons/ri";
@@ -109,6 +110,9 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && ( {checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} /> <Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
)} )}
{checkAccess(user, ["developer", "admin", "teacher", "student"], permissions) && (
<Nav disabled={disableNavigation} Icon={BsPeople} label="Groups" path={path} keyPath="/groups" isMinimized={isMinimized} />
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && ( {checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} /> <Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
)} )}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -62,7 +62,7 @@ export default function CorporateDashboard({user}: Props) {
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({admin: user.id});
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);

View File

@@ -1,140 +1,108 @@
import React from "react"; import React from "react";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import { User } from "@/interfaces/user"; import {User} from "@/interfaces/user";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import ProgressBar from "@/components/Low/ProgressBar"; import ProgressBar from "@/components/Low/ProgressBar";
import { import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
BsBook, import {MODULE_ARRAY} from "@/utils/moduleUtils";
BsClipboard, import {capitalize} from "lodash";
BsHeadphones, import {getLevelLabel} from "@/utils/score";
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 }) => { const Card = ({user}: {user: User}) => {
return ( 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="border-mti-gray-platinum flex flex-col h-fit w-full cursor-pointer gap-6 rounded-xl border bg-white p-4 transition duration-300 ease-in-out hover:drop-shadow">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<h3 className="text-xl font-semibold">{user.name}</h3> <h3 className="text-xl font-semibold">{user.name}</h3>
</div> </div>
<div className="flex w-full gap-3 flex-wrap"> <div className="flex w-full gap-3 flex-wrap">
{MODULE_ARRAY.map((module) => { {MODULE_ARRAY.map((module) => {
const desiredLevel = user.desiredLevels[module] || 9; const desiredLevel = user.desiredLevels[module] || 9;
const level = user.levels[module] || 0; const level = user.levels[module] || 0;
return ( return (
<div <div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]" key={module}>
className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 min-w-[250px]" <div className="flex items-center gap-2 md:gap-3">
key={module} <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" />}
<div className="flex items-center gap-2 md:gap-3"> {module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
<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 === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
{module === "reading" && ( {module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
<BsBook className="text-ielts-reading 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>
{module === "listening" && ( <div className="flex w-full flex-col">
<BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" /> <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 === "writing" && ( {module === "level" && <span>English Level: {getLevelLabel(level).join(" / ")}</span>}
<BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" /> {module !== "level" && (
)} <div className="flex flex-col">
{module === "speaking" && ( <span>Level {level} / Level 9</span>
<BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" /> <span>Desired Level: {desiredLevel}</span>
)} </div>
{module === "level" && ( )}
<BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" /> </div>
)} </div>
</div> </div>
<div className="flex w-full flex-col"> <div className="md:pl-14">
<span className="text-sm font-bold md:font-extrabold w-full"> <ProgressBar
{capitalize(module)} color={module}
</span> label=""
<div className="text-mti-gray-dim text-sm font-normal"> mark={Math.round((desiredLevel * 100) / 9)}
{module === "level" && ( markLabel={`Desired Level: ${desiredLevel}`}
<span> percentage={Math.round((level * 100) / 9)}
English Level: {getLevelLabel(level).join(" / ")} className="h-2 w-full"
</span> />
)} </div>
{module !== "level" && ( </div>
<div className="flex flex-col"> );
<span>Level {level} / Level 9</span> })}
<span>Desired Level: {desiredLevel}</span> </div>
</div> </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 CorporateStudentsLevels = () => {
const { users } = useUsers(); const {users} = useUsers();
const { groups } = useGroups(); const {groups} = useGroups({});
const corporateUsers = users.filter((u) => u.type === "corporate") as User[]; const corporateUsers = users.filter((u) => u.type === "corporate") as User[];
const [corporateId, setCorporateId] = React.useState<string>(""); const [corporateId, setCorporateId] = React.useState<string>("");
const corporate = const corporate = corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
corporateUsers.find((u) => u.id === corporateId) || corporateUsers[0];
const groupsFromCorporate = corporate const groupsFromCorporate = corporate ? groups.filter((g) => g.admin === corporate.id) : [];
? groups.filter((g) => g.admin === corporate.id)
: [];
const groupsParticipants = groupsFromCorporate const groupsParticipants = groupsFromCorporate
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.reduce((accm: User[], p) => { .reduce((accm: User[], p) => {
const user = users.find((u) => u.id === p) as User; const user = users.find((u) => u.id === p) as User;
if (user) { if (user) {
return [...accm, user]; return [...accm, user];
} }
return accm; return accm;
}, []); }, []);
return ( return (
<> <>
<Select <Select
options={corporateUsers.map((x: User) => ({ options={corporateUsers.map((x: User) => ({
value: x.id, value: x.id,
label: `${x.name} - ${x.email}`, label: `${x.name} - ${x.email}`,
}))} }))}
value={corporate ? { value: corporate.id, label: corporate.name } : null} value={corporate ? {value: corporate.id, label: corporate.name} : null}
onChange={(value) => setCorporateId(value?.value!)} onChange={(value) => setCorporateId(value?.value!)}
styles={{ styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }), menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
? "#D5D9F0" color: state.isFocused ? "black" : styles.color,
: state.isSelected }),
? "#7872BF" }}
: "white", />
color: state.isFocused ? "black" : styles.color, {groupsParticipants.map((u) => (
}), <Card user={u} key={u.id} />
}} ))}
/> </>
{groupsParticipants.map((u) => ( );
<Card user={u} key={u.id} />
))}
</>
);
}; };
export default CorporateStudentsLevels; export default CorporateStudentsLevels;

View File

@@ -54,7 +54,7 @@ export default function MasterCorporateDashboard({user}: Props) {
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, user.type); const {groups} = useGroups({admin: user.id, userType: user.type});
const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))]; const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))]; const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];

View File

@@ -63,7 +63,7 @@ export default function TeacherDashboard({user}: Props) {
const {stats} = useStats(); const {stats} = useStats();
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {groups} = useGroups(user.id); const {groups} = useGroups({admin: user.id});
const {permissions} = usePermissions(user.id); const {permissions} = usePermissions(user.id);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});

View File

@@ -2,7 +2,13 @@ import {Group, User} from "@/interfaces/user";
import axios from "axios"; import axios from "axios";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
export default function useGroups(admin?: string, userType?: string, teacher?: string) { interface Props {
admin?: string;
userType?: string;
adminAdmins?: string;
}
export default function useGroups({admin, userType, adminAdmins}: Props) {
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
@@ -12,24 +18,24 @@ export default function useGroups(admin?: string, userType?: string, teacher?: s
const getData = () => { const getData = () => {
setIsLoading(true); setIsLoading(true);
const url = admin && !teacher ? `/api/groups?admin=${admin}` : "/api/groups"; const url = admin && !adminAdmins ? `/api/groups?admin=${admin}` : "/api/groups";
axios axios
.get<Group[]>(url) .get<Group[]>(url)
.then((response) => { .then((response) => {
if (isMasterType) return setGroups(response.data); if (isMasterType) return setGroups(response.data);
const filterByAdmins = !!teacher const filterByAdmins = !!adminAdmins
? [teacher, ...response.data.filter((g) => g.participants.includes(teacher)).flatMap((g) => g.admin)] ? [adminAdmins, ...response.data.filter((g) => g.participants.includes(adminAdmins)).flatMap((g) => g.admin)]
: [admin]; : [admin];
const filter = (g: Group) => filterByAdmins.includes(g.admin) || g.participants.includes(admin || ""); const adminFilter = (g: Group) => filterByAdmins.includes(g.admin) || g.participants.includes(admin || "");
const filteredGroups = !!admin || !!teacher ? response.data.filter(filter) : response.data; const filteredGroups = !!admin || !!adminAdmins ? response.data.filter(adminFilter) : response.data;
return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups); return setGroups(admin ? filteredGroups.map((g) => ({...g, disableEditing: g.disableEditing || g.admin !== admin})) : filteredGroups);
}) })
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}; };
useEffect(getData, [admin, teacher, isMasterType]); useEffect(getData, [admin, adminAdmins, isMasterType]);
return {groups, isLoading, isError, reload: getData}; return {groups, isLoading, isError, reload: getData};
} }

View File

@@ -201,11 +201,11 @@ export default function GroupList({user}: {user: User}) {
const {permissions} = usePermissions(user?.id || ""); const {permissions} = usePermissions(user?.id || "");
const {users} = useUsers(); const {users} = useUsers();
const {groups, reload} = useGroups( const {groups, reload} = useGroups({
user && filterTypes.includes(user?.type) ? user.id : undefined, admin: user && filterTypes.includes(user?.type) ? user.id : undefined,
user?.type, userType: user?.type,
user?.type === "teacher" ? user?.id : undefined, adminAdmins: user?.type === "teacher" ? user?.id : undefined,
); });
useEffect(() => { useEffect(() => {
if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) { if (user && ["corporate", "teacher", "mastercorporate"].includes(user.type)) {

View File

@@ -58,7 +58,7 @@ export default function UserList({
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {permissions} = usePermissions(user?.id || ""); const {permissions} = usePermissions(user?.id || "");
const {groups} = useGroups(user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined); const {groups} = useGroups({admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined});
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();

View File

@@ -31,7 +31,7 @@ export default function PaymentDue({user, hasExpired = false, reload}: Props) {
const {packages} = usePackages(); const {packages} = usePackages();
const {discounts} = useDiscounts(); const {discounts} = useDiscounts();
const {users} = useUsers(); const {users} = useUsers();
const {groups} = useGroups(); const {groups} = useGroups({});
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id}); const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id});
useEffect(() => { useEffect(() => {

119
src/pages/groups.tsx Normal file
View File

@@ -0,0 +1,119 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import Navbar from "@/components/Navbar";
import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone} from "react-icons/bs";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {useEffect, useState} from "react";
import useStats from "@/hooks/useStats";
import {averageScore, groupBySession, totalExams} from "@/utils/stats";
import useUser from "@/hooks/useUser";
import Diagnostic from "@/components/Diagnostic";
import {ToastContainer} from "react-toastify";
import {capitalize} from "lodash";
import {Module} from "@/interfaces";
import ProgressBar from "@/components/Low/ProgressBar";
import Layout from "@/components/High/Layout";
import {calculateAverageLevel} from "@/utils/score";
import axios from "axios";
import DemographicInformationInput from "@/components/DemographicInformationInput";
import moment from "moment";
import Link from "next/link";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import ProfileSummary from "@/components/ProfileSummary";
import StudentDashboard from "@/dashboards/Student";
import AdminDashboard from "@/dashboards/Admin";
import CorporateDashboard from "@/dashboards/Corporate";
import TeacherDashboard from "@/dashboards/Teacher";
import AgentDashboard from "@/dashboards/Agent";
import MasterCorporateDashboard from "@/dashboards/MasterCorporate";
import PaymentDue from "./(status)/PaymentDue";
import {useRouter} from "next/router";
import {PayPalScriptProvider} from "@paypal/react-paypal-js";
import {CorporateUser, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user";
import Select from "react-select";
import {USER_TYPE_LABELS} from "@/resources/user";
import {checkAccess, getTypesOfUser} from "@/utils/permissions";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useGroups from "@/hooks/useGroups";
import useUsers from "@/hooks/useUsers";
import {getUserName} from "@/utils/users";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
if (shouldRedirectHome(user)) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {user: req.session.user},
};
}, sessionOptions);
interface Props {
user: User;
envVariables: {[key: string]: string};
}
export default function Home(props: Props) {
const {user, mutateUser} = useUser({redirectTo: "/login"});
const {groups} = useGroups({});
const {users} = useUsers();
const router = useRouter();
useEffect(() => {
console.log(groups);
}, [groups]);
return (
<>
<Head>
<title>EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user}>
<div className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{groups
.filter((x) => x.participants.includes(user.id))
.map((group) => (
<div key={group.id} className="p-4 border rounded-xl flex flex-col gap-2">
<span>
<b>Group: </b>
{group.name}
</span>
<span>
<b>Admin: </b>
{getUserName(users.find((x) => x.id === group.admin))}
</span>
<b>Participants: </b>
<span>{group.participants.map((x) => getUserName(users.find((u) => u.id === x))).join(", ")}</span>
</div>
))}
</div>
</Layout>
)}
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,29 @@
/* 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, useRef, useState } from "react"; import {useEffect, useRef, useState} from "react";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import { groupByDate } from "@/utils/stats"; import {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 { ToastContainer } from "react-toastify"; import {ToastContainer} from "react-toastify";
import { useRouter } from "next/router"; import {useRouter} from "next/router";
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import clsx from "clsx"; import clsx from "clsx";
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"; import useRecordStore from "@/stores/recordStore";
import useTrainingContentStore from "@/stores/trainingContentStore"; import useTrainingContentStore from "@/stores/trainingContentStore";
import StatsGridItem from "@/components/StatGridItem"; import StatsGridItem from "@/components/StatGridItem";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
@@ -46,7 +45,7 @@ export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
} }
return { return {
props: { user: req.session.user }, props: {user: req.session.user},
}; };
}, sessionOptions); }, sessionOptions);
@@ -55,16 +54,21 @@ const defaultSelectableCorporate = {
label: "All", label: "All",
}; };
export default function History({ user }: { user: User }) { export default function History({user}: {user: User}) {
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.training, state.setTraining]); const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser,
state.training,
state.setTraining,
]);
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id); // const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>(); const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">(); const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
const { assignments } = useAssignments({}); const {assignments} = useAssignments({});
const { users } = useUsers(); const {users} = useUsers();
const { stats, isLoading: isStatsLoading } = useStats(statsUserId); const {stats, isLoading: isStatsLoading} = useStats(statsUserId);
const { groups: allGroups } = useGroups(); const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id); const groups = allGroups.filter((x) => x.admin === user.id);
@@ -104,12 +108,12 @@ export default function History({ user }: { user: User }) {
setFilter((prev) => (prev === value ? undefined : value)); setFilter((prev) => (prev === value ? undefined : value));
}; };
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => { const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
if (filter && filter !== "assignments") { if (filter && filter !== "assignments") {
const filterDate = moment() const filterDate = moment()
.subtract({ [filter as string]: 1 }) .subtract({[filter as string]: 1})
.format("x"); .format("x");
const filteredStats: { [key: string]: Stat[] } = {}; const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => { Object.keys(stats).forEach((timestamp) => {
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp]; if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
@@ -118,7 +122,7 @@ export default function History({ user }: { user: User }) {
} }
if (filter && filter === "assignments") { if (filter && filter === "assignments") {
const filteredStats: { [key: string]: Stat[] } = {}; const filteredStats: {[key: string]: Stat[]} = {};
Object.keys(stats).forEach((timestamp) => { Object.keys(stats).forEach((timestamp) => {
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false)) if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
@@ -137,13 +141,13 @@ export default function History({ user }: { user: User }) {
useEffect(() => { useEffect(() => {
const handleRouteChange = (url: string) => { const handleRouteChange = (url: string) => {
setTraining(false) setTraining(false);
} };
router.events.on('routeChangeStart', handleRouteChange) router.events.on("routeChangeStart", handleRouteChange);
return () => { return () => {
router.events.off('routeChangeStart', handleRouteChange) router.events.off("routeChangeStart", handleRouteChange);
} };
}, [router.events, setTraining]) }, [router.events, setTraining]);
const handleTrainingContentSubmission = () => { const handleTrainingContentSubmission = () => {
if (groupedStats) { if (groupedStats) {
@@ -156,11 +160,10 @@ export default function History({ user }: { user: User }) {
} }
return accumulator; return accumulator;
}, {}); }, {});
setTrainingStats(Object.values(selectedStats).flat()) setTrainingStats(Object.values(selectedStats).flat());
router.push("/training"); router.push("/training");
} }
} };
const customContent = (timestamp: string) => { const customContent = (timestamp: string) => {
if (!groupedStats) return <></>; if (!groupedStats) return <></>;
@@ -240,13 +243,13 @@ export default function History({ user }: { user: User }) {
const selectedUser = getSelectedUser(); const selectedUser = getSelectedUser();
const selectedUserSelectValue = selectedUser const selectedUserSelectValue = selectedUser
? { ? {
value: selectedUser.id, value: selectedUser.id,
label: `${selectedUser.name} - ${selectedUser.email}`, label: `${selectedUser.name} - ${selectedUser.email}`,
} }
: { : {
value: "", value: "",
label: "", label: "",
}; };
return ( return (
<> <>
<Head> <Head>
@@ -272,7 +275,7 @@ export default function History({ user }: { user: User }) {
value={selectableCorporates.find((x) => x.value === selectedCorporate)} value={selectableCorporates.find((x) => x.value === selectedCorporate)}
onChange={(value) => setSelectedCorporate(value?.value || "")} onChange={(value) => setSelectedCorporate(value?.value || "")}
styles={{ styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }), menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -289,7 +292,7 @@ export default function History({ user }: { user: User }) {
value={selectedUserSelectValue} value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)} onChange={(value) => setStatsUserId(value?.value!)}
styles={{ styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }), menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -313,7 +316,7 @@ export default function History({ user }: { user: User }) {
value={selectedUserSelectValue} value={selectedUserSelectValue}
onChange={(value) => setStatsUserId(value?.value!)} onChange={(value) => setStatsUserId(value?.value!)}
styles={{ styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }), menuPortal: (base) => ({...base, zIndex: 9999}),
option: (styles, state) => ({ option: (styles, state) => ({
...styles, ...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
@@ -323,10 +326,12 @@ export default function History({ user }: { user: User }) {
/> />
</> </>
)} )}
{(training && ( {training && (
<div className="flex flex-row"> <div className="flex flex-row">
<div className="font-semibold text-2xl mr-4">Select up to 10 exercises <div className="font-semibold text-2xl mr-4">
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}</div> Select up to 10 exercises
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
</div>
<button <button
className={clsx( className={clsx(
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed", "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
@@ -337,7 +342,7 @@ export default function History({ user }: { user: User }) {
Submit Submit
</button> </button>
</div> </div>
))} )}
</div> </div>
<div className="flex gap-4 w-full justify-center xl:justify-end"> <div className="flex gap-4 w-full justify-center xl:justify-end">
<button <button

View File

@@ -71,7 +71,7 @@ export default function Stats() {
const {user} = useUser({redirectTo: "/login"}); const {user} = useUser({redirectTo: "/login"});
const {users} = useUsers(); const {users} = useUsers();
const {groups} = useGroups(user?.id); const {groups} = useGroups({admin: user?.id});
const {stats} = useStats(statsUserId, !statsUserId); const {stats} = useStats(statsUserId, !statsUserId);
useEffect(() => { useEffect(() => {
@@ -202,7 +202,7 @@ export default function Stats() {
}} }}
/> />
)} )}
{(["corporate", "teacher", "mastercorporate"].includes(user.type) ) && groups.length > 0 && ( {["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
<Select <Select
className="w-full" className="w-full"
options={users options={users

View File

@@ -1,14 +1,14 @@
/* 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 { ToastContainer } from "react-toastify"; import {ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout"; import Layout from "@/components/High/Layout";
import { shouldRedirectHome } from "@/utils/navigation.disabled"; import {shouldRedirectHome} from "@/utils/navigation.disabled";
import { use, useEffect, useState } from "react"; import {use, useEffect, useState} from "react";
import clsx from "clsx"; import clsx from "clsx";
import { FaPlus } from "react-icons/fa"; import {FaPlus} from "react-icons/fa";
import useRecordStore from "@/stores/recordStore"; import useRecordStore from "@/stores/recordStore";
import router from "next/router"; import router from "next/router";
import useTrainingContentStore from "@/stores/trainingContentStore"; import useTrainingContentStore from "@/stores/trainingContentStore";
@@ -16,390 +16,388 @@ import axios from "axios";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import { ITrainingContent } from "@/training/TrainingInterfaces"; import {ITrainingContent} from "@/training/TrainingInterfaces";
import moment from "moment"; import moment from "moment";
import { uuidv4 } from "@firebase/util"; import {uuidv4} from "@firebase/util";
import TrainingScore from "@/training/TrainingScore"; import TrainingScore from "@/training/TrainingScore";
import ModuleBadge from "@/components/ModuleBadge"; import ModuleBadge from "@/components/ModuleBadge";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
if (!user || !user.isVerified) { if (!user || !user.isVerified) {
return { return {
redirect: { redirect: {
destination: "/login", destination: "/login",
permanent: false, permanent: false,
}, },
}; };
} }
if (shouldRedirectHome(user)) { if (shouldRedirectHome(user)) {
return { return {
redirect: { redirect: {
destination: "/", destination: "/",
permanent: false, permanent: false,
}, },
}; };
} }
return { return {
props: { user: req.session.user }, props: {user: req.session.user},
}; };
}, sessionOptions); }, sessionOptions);
const defaultSelectableCorporate = { const defaultSelectableCorporate = {
value: "", value: "",
label: "All", label: "All",
}; };
const Training: React.FC<{ user: User }> = ({ user }) => { const Training: React.FC<{user: User}> = ({user}) => {
// Record stuff // Record stuff
const { users } = useUsers(); const {users} = useUsers();
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value); const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setSelectedUser, state.setTraining]); const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [
const { groups: allGroups } = useGroups(); state.selectedUser,
const groups = allGroups.filter((x) => x.admin === user.id); state.setSelectedUser,
const [filter, setFilter] = useState<"months" | "weeks" | "days">(); state.setTraining,
]);
const {groups: allGroups} = useGroups({});
const groups = allGroups.filter((x) => x.admin === user.id);
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
const toggleFilter = (value: "months" | "weeks" | "days") => { const toggleFilter = (value: "months" | "weeks" | "days") => {
setFilter((prev) => (prev === value ? undefined : value)); setFilter((prev) => (prev === value ? undefined : value));
}; };
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]); const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]); const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0); const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>(); const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{[key: string]: ITrainingContent}>();
useEffect(() => { useEffect(() => {
const handleRouteChange = (url: string) => { const handleRouteChange = (url: string) => {
setTrainingStats([]) setTrainingStats([]);
} };
router.events.on('routeChangeStart', handleRouteChange) router.events.on("routeChangeStart", handleRouteChange);
return () => { return () => {
router.events.off('routeChangeStart', handleRouteChange) router.events.off("routeChangeStart", handleRouteChange);
} };
}, [router.events, setTrainingStats]) }, [router.events, setTrainingStats]);
useEffect(() => { useEffect(() => {
const postStats = async () => { const postStats = async () => {
try { try {
const response = await axios.post<{ id: string }>(`/api/training`, stats); const response = await axios.post<{id: string}>(`/api/training`, stats);
return response.data.id; return response.data.id;
} catch (error) { } catch (error) {
setIsNewContentLoading(false); setIsNewContentLoading(false);
} }
}; };
if (isNewContentLoading) { if (isNewContentLoading) {
postStats().then(id => { postStats().then((id) => {
setTrainingStats([]); setTrainingStats([]);
if (id) { if (id) {
router.push(`/training/${id}`) router.push(`/training/${id}`);
} }
}); });
} }
}, [isNewContentLoading]) }, [isNewContentLoading]);
useEffect(() => { useEffect(() => {
const loadTrainingContent = async () => { const loadTrainingContent = async () => {
try { try {
const response = await axios.get<ITrainingContent[]>('/api/training'); const response = await axios.get<ITrainingContent[]>("/api/training");
setTrainingContent(response.data); setTrainingContent(response.data);
setIsLoading(false); setIsLoading(false);
} catch (error) { } catch (error) {
setTrainingContent([]); setTrainingContent([]);
setIsLoading(false); setIsLoading(false);
} }
}; };
loadTrainingContent(); loadTrainingContent();
}, []); }, []);
const handleNewTrainingContent = () => { const handleNewTrainingContent = () => {
setRecordTraining(true); setRecordTraining(true);
router.push('/record') router.push("/record");
} };
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
if (filter) {
const filterDate = moment()
.subtract({[filter as string]: 1})
.format("x");
const filteredTrainingContent: {[key: string]: ITrainingContent} = {};
const filterTrainingContentByDate = (trainingContent: { [key: string]: ITrainingContent }) => { Object.keys(trainingContent).forEach((timestamp) => {
if (filter) { if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
const filterDate = moment() });
.subtract({ [filter as string]: 1 }) return filteredTrainingContent;
.format("x"); }
const filteredTrainingContent: { [key: string]: ITrainingContent } = {}; return trainingContent;
};
Object.keys(trainingContent).forEach((timestamp) => { useEffect(() => {
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp]; if (trainingContent.length > 0) {
}); const grouped = trainingContent.reduce((acc, content) => {
return filteredTrainingContent; acc[content.created_at] = content;
} return acc;
return trainingContent; }, {} as {[key: number]: ITrainingContent});
};
useEffect(() => { setGroupedByTrainingContent(grouped);
if (trainingContent.length > 0) { }
const grouped = trainingContent.reduce((acc, content) => { }, [trainingContent]);
acc[content.created_at] = content;
return acc;
}, {} as { [key: number]: ITrainingContent });
setGroupedByTrainingContent(grouped); // Record Stuff
} const selectableCorporates = [
}, [trainingContent]) defaultSelectableCorporate,
...users
.filter((x) => x.type === "corporate")
.map((x) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
})),
];
const getUsersList = (): User[] => {
if (selectedCorporate) {
// get groups for that corporate
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
// Record Stuff // get the teacher ids for that group
const selectableCorporates = [ const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
defaultSelectableCorporate,
...users
.filter((x) => x.type === "corporate")
.map((x) => ({
value: x.id,
label: `${x.name} - ${x.email}`,
})),
];
const getUsersList = (): User[] => { // // search for groups for these teachers
if (selectedCorporate) { // const teacherGroups = allGroups.filter((x) => {
// get groups for that corporate // return selectedCorporateGroupsParticipants.includes(x.admin);
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate); // });
// get the teacher ids for that group // const usersList = [
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants); // ...selectedCorporateGroupsParticipants,
// ...teacherGroups.flatMap((x) => x.participants),
// ];
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
return userListWithUsers.filter((x) => x);
}
// // search for groups for these teachers return users || [];
// const teacherGroups = allGroups.filter((x) => { };
// return selectedCorporateGroupsParticipants.includes(x.admin);
// });
// const usersList = [ const corporateFilteredUserList = getUsersList();
// ...selectedCorporateGroupsParticipants, const getSelectedUser = () => {
// ...teacherGroups.flatMap((x) => x.participants), if (selectedCorporate) {
// ]; const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[]; return userInCorporate || corporateFilteredUserList[0];
return userListWithUsers.filter((x) => x); }
}
return users || []; return users.find((x) => x.id === statsUserId) || user;
}; };
const corporateFilteredUserList = getUsersList(); const selectedUser = getSelectedUser();
const getSelectedUser = () => { const selectedUserSelectValue = selectedUser
if (selectedCorporate) { ? {
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId); value: selectedUser.id,
return userInCorporate || corporateFilteredUserList[0]; label: `${selectedUser.name} - ${selectedUser.email}`,
} }
: {
value: "",
label: "",
};
return users.find((x) => x.id === statsUserId) || user; const formatTimestamp = (timestamp: string) => {
}; const date = moment(parseInt(timestamp));
const formatter = "YYYY/MM/DD - HH:mm";
const selectedUser = getSelectedUser(); return date.format(formatter);
const selectedUserSelectValue = selectedUser };
? {
value: selectedUser.id,
label: `${selectedUser.name} - ${selectedUser.email}`,
}
: {
value: "",
label: "",
};
const formatTimestamp = (timestamp: string) => { const selectTrainingContent = (trainingContent: ITrainingContent) => {
const date = moment(parseInt(timestamp)); router.push(`/training/${trainingContent.id}`);
const formatter = "YYYY/MM/DD - HH:mm"; };
return date.format(formatter); const trainingContentContainer = (timestamp: string) => {
}; if (!groupedByTrainingContent) return <></>;
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
const selectTrainingContent = (trainingContent: ITrainingContent) => { return (
router.push(`/training/${trainingContent.id}`) <>
}; <div
key={uuidv4()}
className={clsx(
"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",
)}
onClick={() => selectTrainingContent(trainingContent)}
role="button">
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
<span className="font-medium">{formatTimestamp(timestamp)}</span>
</div>
<div className="flex flex-col gap-2">
<div className="w-full flex flex-row gap-1">
{uniqueModules.map((module) => (
<ModuleBadge key={module} module={module} />
))}
</div>
</div>
</div>
<TrainingScore trainingContent={trainingContent} gridView={true} />
</div>
</>
);
};
return (
<>
<Head>
<title>Training | 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 />
const trainingContentContainer = (timestamp: string) => { <Layout user={user}>
if (!groupedByTrainingContent) return <></>; {isNewContentLoading || isLoading ? (
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp]; <div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
const uniqueModules = [...new Set(trainingContent.exams.map(exam => exam.module))]; <span className="loading loading-infinity w-32 bg-mti-green-light" />
{isNewContentLoading && (
<span className="text-center text-2xl font-bold text-mti-green-light">Assessing your exams, please be patient...</span>
)}
</div>
) : (
<>
<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>
return ( <Select
<> options={selectableCorporates}
<div value={selectableCorporates.find((x) => x.value === selectedCorporate)}
key={uuidv4()} onChange={(value) => setSelectedCorporate(value?.value || "")}
className={clsx( styles={{
"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" menuPortal: (base) => ({...base, zIndex: 9999}),
)} option: (styles, state) => ({
onClick={() => selectTrainingContent(trainingContent)} ...styles,
role="button"> backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
<div className="w-full flex justify-between -md:items-center 2xl:items-center"> color: state.isFocused ? "black" : styles.color,
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2"> }),
<span className="font-medium">{formatTimestamp(timestamp)}</span> }}></Select>
</div> <label className="font-normal text-base text-mti-gray-dim">User</label>
<div className="flex flex-col gap-2">
<div className="w-full flex flex-row gap-1">
{uniqueModules.map((module) => (
<ModuleBadge key={module} module={module} />
))}
</div>
</div>
</div>
<TrainingScore
trainingContent={trainingContent}
gridView={true}
/>
</div>
</>
);
};
return ( <Select
<> options={corporateFilteredUserList.map((x) => ({
<Head> value: x.id,
<title>Training | EnCoach</title> label: `${x.name} - ${x.email}`,
<meta }))}
name="description" value={selectedUserSelectValue}
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop." onChange={(value) => setStatsUserId(value?.value!)}
/> styles={{
<meta name="viewport" content="width=device-width, initial-scale=1" /> menuPortal: (base) => ({...base, zIndex: 9999}),
<link rel="icon" href="/favicon.ico" /> option: (styles, state) => ({
</Head> ...styles,
<ToastContainer /> 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>
<Layout user={user}> <Select
{(isNewContentLoading || isLoading ? ( options={users
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12"> .filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
<span className="loading loading-infinity w-32 bg-mti-green-light" /> .map((x) => ({
{isNewContentLoading && (<span className="text-center text-2xl font-bold text-mti-green-light"> value: x.id,
Assessing your exams, please be patient... label: `${x.name} - ${x.email}`,
</span>)} }))}
</div> value={selectedUserSelectValue}
) : ( onChange={(value) => setStatsUserId(value?.value!)}
<> styles={{
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center"> menuPortal: (base) => ({...base, zIndex: 9999}),
<div className="xl:w-3/4"> option: (styles, state) => ({
{(user.type === "developer" || user.type === "admin") && ( ...styles,
<> backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
<label className="font-normal text-base text-mti-gray-dim">Corporate</label> color: state.isFocused ? "black" : styles.color,
}),
<Select }}
options={selectableCorporates} />
value={selectableCorporates.find((x) => x.value === selectedCorporate)} </>
onChange={(value) => setSelectedCorporate(value?.value || "")} )}
styles={{ {user.type === "student" && (
menuPortal: (base) => ({ ...base, zIndex: 9999 }), <>
option: (styles, state) => ({ <div className="flex items-center">
...styles, <div className="font-semibold text-2xl">Generate New Training Material</div>
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", <button
color: state.isFocused ? "black" : styles.color, className={clsx(
}), "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4",
}}></Select> "transition duration-300 ease-in-out",
<label className="font-normal text-base text-mti-gray-dim">User</label> )}
onClick={handleNewTrainingContent}>
<Select <FaPlus />
options={corporateFilteredUserList.map((x) => ({ </button>
value: x.id, </div>
label: `${x.name} - ${x.email}`, </>
}))} )}
value={selectedUserSelectValue} </div>
onChange={(value) => setStatsUserId(value?.value!)} <div className="flex gap-4 w-full justify-center xl:justify-end">
styles={{ <button
menuPortal: (base) => ({ ...base, zIndex: 9999 }), className={clsx(
option: (styles, state) => ({ "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
...styles, "transition duration-300 ease-in-out",
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", filter === "months" && "!bg-mti-purple-light !text-white",
color: state.isFocused ? "black" : styles.color, )}
}), onClick={() => toggleFilter("months")}>
}} Last month
/> </button>
</> <button
)} className={clsx(
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && ( "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",
<label className="font-normal text-base text-mti-gray-dim">User</label> filter === "weeks" && "!bg-mti-purple-light !text-white",
)}
<Select onClick={() => toggleFilter("weeks")}>
options={users Last week
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id)) </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",
value={selectedUserSelectValue} filter === "days" && "!bg-mti-purple-light !text-white",
onChange={(value) => setStatsUserId(value?.value!)} )}
styles={{ onClick={() => toggleFilter("days")}>
menuPortal: (base) => ({ ...base, zIndex: 9999 }), Last day
option: (styles, state) => ({ </button>
...styles, </div>
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", </div>
color: state.isFocused ? "black" : styles.color, {trainingContent.length == 0 && (
}), <div className="flex flex-grow justify-center items-center">
}} <span className="font-semibold ml-1">No training content to display...</span>
/> </div>
</> )}
)} {groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
{(user.type === "student" && ( <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
<> {Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
<div className="flex items-center"> .sort((a, b) => parseInt(b) - parseInt(a))
<div className="font-semibold text-2xl">Generate New Training Material</div> .map(trainingContentContainer)}
<button </div>
className={clsx( )}
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4", </>
"transition duration-300 ease-in-out", )}
)} </Layout>
onClick={handleNewTrainingContent}> </>
<FaPlus /> );
</button> };
</div>
</>
))}
</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 === "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>
{trainingContent.length == 0 && (
<div className="flex flex-grow justify-center items-center">
<span className="font-semibold ml-1">No training content to display...</span>
</div>
)}
{groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
.sort((a, b) => parseInt(b) - parseInt(a))
.map(trainingContentContainer)}
</div>
)}
</>
))}
</Layout>
</>
);
}
export default Training; export default Training;