Standardized the access to the list of users

This commit is contained in:
Joao Ramos
2024-06-19 21:59:35 +01:00
parent a29b0b56d9
commit c11906a395
6 changed files with 2635 additions and 1994 deletions

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

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

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>
)}
</>
);
} }