Updated the MasterCorporate and Corporate pages to allow to have Assignments

This commit is contained in:
Tiago Ribeiro
2024-08-13 10:02:40 +01:00
parent 8162567e12
commit 2784117862
10 changed files with 1050 additions and 819 deletions

View File

@@ -10,6 +10,7 @@ import {usePDFDownload} from "@/hooks/usePDFDownload";
import {useAssignmentArchive} from "@/hooks/useAssignmentArchive"; import {useAssignmentArchive} from "@/hooks/useAssignmentArchive";
import {uniqBy} from "lodash"; import {uniqBy} from "lodash";
import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive"; import {useAssignmentUnarchive} from "@/hooks/useAssignmentUnarchive";
import {getUserName} from "@/utils/users";
interface Props { interface Props {
onClick?: () => void; onClick?: () => void;
@@ -35,6 +36,8 @@ export default function AssignmentCard({
allowArchive, allowArchive,
allowUnarchive, allowUnarchive,
}: Assignment & Props) { }: Assignment & Props) {
const {users} = useUsers();
const renderPdfIcon = usePDFDownload("assignments"); const renderPdfIcon = usePDFDownload("assignments");
const renderArchiveIcon = useAssignmentArchive(id, reload); const renderArchiveIcon = useAssignmentArchive(id, reload);
const renderUnarchiveIcon = useAssignmentUnarchive(id, reload); const renderUnarchiveIcon = useAssignmentUnarchive(id, reload);
@@ -72,11 +75,14 @@ export default function AssignmentCard({
textClassName={results.length / assignees.length < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"} textClassName={results.length / assignees.length < 0.5 ? "!text-mti-gray-dim font-light" : "text-white"}
/> />
</div> </div>
<span className="flex justify-between gap-1"> <div className="flex flex-col gap-1">
<span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span> <span className="flex justify-between gap-1">
<span>-</span> <span>{moment(startDate).format("DD/MM/YY, HH:mm")}</span>
<span>{moment(endDate).format("DD/MM/YY, HH:mm")}</span> <span>-</span>
</span> <span>{moment(endDate).format("DD/MM/YY, HH:mm")}</span>
</span>
<span>Assigner: {getUserName(users.find((x) => x.id === assigner))}</span>
</div>
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2"> <div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
{uniqBy(exams, (x) => x.module).map(({module}) => ( {uniqBy(exams, (x) => x.module).map(({module}) => (
<div <div

View File

@@ -10,6 +10,7 @@ import {getExamById} from "@/utils/exams";
import {sortByModule} from "@/utils/moduleUtils"; import {sortByModule} from "@/utils/moduleUtils";
import {calculateBandScore} from "@/utils/score"; import {calculateBandScore} from "@/utils/score";
import {convertToUserSolutions} from "@/utils/stats"; import {convertToUserSolutions} from "@/utils/stats";
import {getUserName} from "@/utils/users";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {capitalize, uniqBy} from "lodash"; import {capitalize, uniqBy} from "lodash";
@@ -241,13 +242,16 @@ export default function AssignmentView({isOpen, assignment, onClose}: Props) {
<span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span> <span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span>
<span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span> <span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span>
</div> </div>
<span> <div className="flex flex-col gap-2">
Assignees:{" "} <span>
{users Assignees:{" "}
.filter((u) => assignment?.assignees.includes(u.id)) {users
.map((u) => `${u.name} (${u.email})`) .filter((u) => assignment?.assignees.includes(u.id))
.join(", ")} .map((u) => `${u.name} (${u.email})`)
</span> .join(", ")}
</span>
<span>Assigner: {getUserName(users.find((x) => x.id === assignment?.assigner))}</span>
</div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-xl font-bold">Average Scores</span> <span className="text-xl font-bold">Average Scores</span>

View File

@@ -2,412 +2,492 @@
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,
BsArrowRepeat,
BsPlus,
BsEnvelopePaper,
} 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";
import { getUserCorporate } from "@/utils/groups"; import {getUserCorporate} from "@/utils/groups";
import useAssignments from "@/hooks/useAssignments";
import {Assignment} from "@/interfaces/results";
import AssignmentView from "./AssignmentView";
import AssignmentCreator from "./AssignmentCreator";
import clsx from "clsx";
import AssignmentCard from "./AssignmentCard";
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 [corporateUserToShow, setCorporateUserToShow] = const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
useState<CorporateUser>(); const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = 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 {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: 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]);
useEffect(() => { useEffect(() => {
// in this case it fetches the master corporate account // in this case it fetches the master corporate account
getUserCorporate(user.id).then(setCorporateUserToShow); getUserCorporate(user.id).then(setCorporateUserToShow);
}, [user]); }, [user]);
const studentFilter = (user: User) => const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
user.type === "student" && const teacherFilter = (user: User) => user.type === "teacher" && groups.flatMap((g) => g.participants).includes(user.id);
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) => const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
<div <div
onClick={() => setSelectedUser(displayUser)} onClick={() => setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
> <img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<img <div className="flex flex-col gap-1 items-start">
src={displayUser.profilePicture} <span>{displayUser.name}</span>
alt={displayUser.name} <span className="text-sm opacity-75">{displayUser.email}</span>
className="rounded-full w-10 h-10" </div>
/> </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 <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">Students ({total})</h2>
<h2 className="text-2xl font-semibold">Students ({total})</h2> </div>
</div> )}
)} />
/> );
); };
};
const TeachersList = () => { const TeachersList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "teacher" && x.type === "teacher" &&
(!!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 <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">Teachers ({total})</h2>
<h2 className="text-2xl font-semibold">Teachers ({total})</h2> </div>
</div> )}
)} />
/> );
); };
};
const GroupsList = () => { const GroupsList = () => {
const filter = (x: Group) => const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
x.admin === user.id || x.participants.includes(user.id);
return ( return (
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
> <BsArrowLeft className="text-xl" />
<BsArrowLeft className="text-xl" /> <span>Back</span>
<span>Back</span> </div>
</div> <h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
<h2 className="text-2xl font-semibold"> </div>
Groups ({groups.filter(filter).length})
</h2>
</div>
<GroupList user={user} /> <GroupList user={user} />
</> </>
); );
}; };
const averageLevelCalculator = (studentStats: Stat[]) => { const AssignmentsPage = () => {
const formattedStats = studentStats const activeFilter = (a: Assignment) =>
.map((s) => ({ moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
focus: users.find((u) => u.id === s.user)?.focus, const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
score: s.score, const archivedFilter = (a: Assignment) => a.archived;
module: s.module, const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
}))
.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 } = { return (
reading: 0, <>
listening: 0, <AssignmentView
writing: 0, isOpen={!!selectedAssignment && !isCreatingAssignment}
speaking: 0, onClose={() => {
level: 0, setSelectedAssignment(undefined);
}; setIsCreatingAssignment(false);
bandScores.forEach((b) => (levels[b.module] += b.level)); 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 calculateAverageLevel(levels); 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 DefaultDashboard = () => ( const levels: {[key in Module]: number} = {
<> reading: 0,
{corporateUserToShow && ( listening: 0,
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1"> writing: 0,
Linked to:{" "} speaking: 0,
<b> level: 0,
{corporateUserToShow?.corporateInformation?.companyInformation };
.name || corporateUserToShow.name} bandScores.forEach((b) => (levels[b.module] += b.level));
</b>
</div>
)}
<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>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between"> return calculateAverageLevel(levels);
<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>
</>
);
return ( const DefaultDashboard = () => (
<> <>
<Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}> {corporateUserToShow && (
<> <div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
{selectedUser && ( Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
<div className="w-full flex flex-col gap-8"> </div>
<UserCard )}
loggedInUser={user} <section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center">
onClose={(shouldReload) => { <IconCard
setSelectedUser(undefined); onClick={() => setPage("students")}
if (shouldReload) reload(); Icon={BsPersonFill}
}} label="Students"
onViewStudents={ value={users.filter(studentFilter).length}
selectedUser.type === "corporate" || color="purple"
selectedUser.type === "teacher" />
? () => { <IconCard
appendUserFilters({ onClick={() => setPage("teachers")}
id: "view-students", Icon={BsPencilSquare}
filter: (x: User) => x.type === "student", label="Teachers"
}); value={users.filter(teacherFilter).length}
appendUserFilters({ color="purple"
id: "belongs-to-admin", />
filter: (x: User) => <IconCard
groups Icon={BsClipboard2Data}
.filter( label="Exams Performed"
(g) => value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
g.admin === selectedUser.id || color="purple"
g.participants.includes(selectedUser.id) />
) <IconCard
.flatMap((g) => g.participants) Icon={BsPaperclip}
.includes(x.id), 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"
/>
<button
disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")}
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
</span>
</span>
</button>
</section>
router.push("/list/users"); <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">
: undefined <span className="p-4">Latest students</span>
} <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
onViewTeachers={ {users
selectedUser.type === "corporate" || .filter(studentFilter)
selectedUser.type === "student" .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
? () => { .map((x) => (
appendUserFilters({ <UserDisplay key={x.id} {...x} />
id: "view-teachers", ))}
filter: (x: User) => x.type === "teacher", </div>
}); </div>
appendUserFilters({ <div className="bg-white shadow flex flex-col rounded-xl w-full">
id: "belongs-to-admin", <span className="p-4">Latest teachers</span>
filter: (x: User) => <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
groups {users
.filter( .filter(teacherFilter)
(g) => .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
g.admin === selectedUser.id || .map((x) => (
g.participants.includes(selectedUser.id) <UserDisplay key={x.id} {...x} />
) ))}
.flatMap((g) => g.participants) </div>
.includes(x.id), </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>
</>
);
router.push("/list/users"); return (
} <>
: undefined <Modal isOpen={showModal} onClose={() => setSelectedUser(undefined)}>
} <>
user={selectedUser} {selectedUser && (
/> <div className="w-full flex flex-col gap-8">
</div> <UserCard
)} loggedInUser={user}
</> onClose={(shouldReload) => {
</Modal> setSelectedUser(undefined);
{page === "students" && <StudentsList />} if (shouldReload) reload();
{page === "teachers" && <TeachersList />} }}
{page === "groups" && <GroupsList />} onViewStudents={
{page === "" && <DefaultDashboard />} selectedUser.type === "corporate" || selectedUser.type === "teacher"
</> ? () => {
); appendUserFilters({
id: "view-students",
filter: (x: User) => x.type === "student",
});
appendUserFilters({
id: "belongs-to-admin",
filter: (x: User) =>
groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
.flatMap((g) => g.participants)
.includes(x.id),
});
router.push("/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 === "assignments" && <AssignmentsPage />}
{page === "" && <DefaultDashboard />}
</>
);
} }

View File

@@ -2,423 +2,496 @@
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 { Group, MasterCorporateUser, Stat, User } from "@/interfaces/user"; import {Group, MasterCorporateUser, 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,
BsClock, BsClock,
BsPaperclip, BsPaperclip,
BsPersonFill, BsPersonFill,
BsPencilSquare, BsPencilSquare,
BsPersonCheck, BsPersonCheck,
BsPeople, BsPeople,
BsBank, BsBank,
BsEnvelopePaper,
BsArrowRepeat,
BsPlus,
} 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";
import useAssignments from "@/hooks/useAssignments";
import {Assignment} from "@/interfaces/results";
import AssignmentView from "./AssignmentView";
import AssignmentCreator from "./AssignmentCreator";
import clsx from "clsx";
import AssignmentCard from "./AssignmentCard";
interface Props { interface Props {
user: MasterCorporateUser; user: MasterCorporateUser;
} }
export default function MasterCorporateDashboard({ user }: Props) { export default function MasterCorporateDashboard({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 [isCreatingAssignment, setIsCreatingAssignment] = 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, user.type); const {groups} = useGroups(user.id, user.type);
const masterCorporateUserGroups = [ const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
...new Set( const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
),
];
const corporateUserGroups = [
...new Set(groups.flatMap((g) => g.participants)),
];
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const router = useRouter();
useEffect(() => { const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
setShowModal(!!selectedUser && page === ""); const router = useRouter();
}, [selectedUser, page]);
const studentFilter = (user: User) => useEffect(() => {
user.type === "student" && corporateUserGroups.includes(user.id); setShowModal(!!selectedUser && page === "");
const teacherFilter = (user: User) => }, [selectedUser, page]);
user.type === "teacher" && corporateUserGroups.includes(user.id);
const getStatsByStudent = (user: User) => const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
stats.filter((s) => s.user === user.id); const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
const UserDisplay = (displayUser: User) => ( const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
<div
onClick={() => setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
>
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
const StudentsList = () => { const UserDisplay = (displayUser: User) => (
const filter = (x: User) => <div
x.type === "student" && onClick={() => setSelectedUser(displayUser)}
(!!selectedUser className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
? corporateUserGroups.includes(x.id) || false <img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
: corporateUserGroups.includes(x.id)); <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span>
</div>
</div>
);
return ( const StudentsList = () => {
<UserList const filter = (x: User) =>
user={user} x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
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">Students ({total})</h2>
</div>
)}
/>
);
};
const TeachersList = () => { return (
const filter = (x: User) => <UserList
x.type === "teacher" && user={user}
(!!selectedUser filters={[filter]}
? corporateUserGroups.includes(x.id) || false renderHeader={(total) => (
: corporateUserGroups.includes(x.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">Students ({total})</h2>
</div>
)}
/>
);
};
return ( const TeachersList = () => {
<UserList const filter = (x: User) =>
user={user} x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
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">Teachers ({total})</h2>
</div>
)}
/>
);
};
const corporateUserFilter = (x: User) => return (
x.type === "corporate" && <UserList
(!!selectedUser user={user}
? masterCorporateUserGroups.includes(x.id) || false filters={[filter]}
: masterCorporateUserGroups.includes(x.id)); 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">Teachers ({total})</h2>
</div>
)}
/>
);
};
const CorporateList = () => { const corporateUserFilter = (x: User) =>
return ( x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
<UserList
user={user}
filters={[corporateUserFilter]}
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">Corporates ({total})</h2>
</div>
)}
/>
);
};
const GroupsList = () => { const CorporateList = () => {
return ( return (
<> <UserList
<div className="flex flex-col gap-4"> user={user}
<div filters={[corporateUserFilter]}
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">
> <div
<BsArrowLeft className="text-xl" /> onClick={() => setPage("")}
<span>Back</span> 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" />
<h2 className="text-2xl font-semibold"> <span>Back</span>
Groups ({groups.length}) </div>
</h2> <h2 className="text-2xl font-semibold">Corporates ({total})</h2>
</div> </div>
)}
/>
);
};
<GroupList user={user} /> const GroupsList = () => {
</> 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.length})</h2>
</div>
const averageLevelCalculator = (studentStats: Stat[]) => { <GroupList user={user} />
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 } = { const AssignmentsPage = () => {
reading: 0, const activeFilter = (a: Assignment) =>
listening: 0, moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
writing: 0, const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
speaking: 0, const archivedFilter = (a: Assignment) => a.archived;
level: 0, const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
};
bandScores.forEach((b) => (levels[b.module] += b.level));
return calculateAverageLevel(levels); return (
}; <>
<AssignmentView
isOpen={!!selectedAssignment && !isCreatingAssignment}
onClose={() => {
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>
</>
);
};
const DefaultDashboard = () => ( const averageLevelCalculator = (studentStats: Stat[]) => {
<> const formattedStats = studentStats
<section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center"> .map((s) => ({
<IconCard focus: users.find((u) => u.id === s.user)?.focus,
onClick={() => setPage("students")} score: s.score,
Icon={BsPersonFill} module: s.module,
label="Students" }))
value={users.filter(studentFilter).length} .filter((f) => !!f.focus);
color="purple" const bandScores = formattedStats.map((s) => ({
/> module: s.module,
<IconCard level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
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"
/>
<IconCard
Icon={BsBank}
label="Corporate"
value={masterCorporateUserGroups.length}
color="purple"
onClick={() => setPage("corporate")}
/>
</section>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between"> const levels: {[key in Module]: number} = {
<div className="bg-white shadow flex flex-col rounded-xl w-full"> reading: 0,
<span className="p-4">Latest students</span> listening: 0,
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> writing: 0,
{users speaking: 0,
.filter(studentFilter) level: 0,
.sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) };
.map((x) => ( bandScores.forEach((b) => (levels[b.module] += b.level));
<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>
</>
);
return ( return calculateAverageLevel(levels);
<> };
<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"
? () => {
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"); const DefaultDashboard = () => (
} <>
: undefined <section className="flex flex-wrap gap-2 items-center -lg:justify-center lg:justify-between text-center">
} <IconCard
onViewTeachers={ onClick={() => setPage("students")}
selectedUser.type === "corporate" || Icon={BsPersonFill}
selectedUser.type === "student" label="Students"
? () => { value={users.filter(studentFilter).length}
appendUserFilters({ color="purple"
id: "view-teachers", />
filter: (x: User) => x.type === "teacher", <IconCard
}); onClick={() => setPage("teachers")}
appendUserFilters({ Icon={BsPencilSquare}
id: "belongs-to-admin", label="Teachers"
filter: (x: User) => value={users.filter(teacherFilter).length}
groups color="purple"
.filter( />
(g) => <IconCard
g.admin === selectedUser.id || Icon={BsClipboard2Data}
g.participants.includes(selectedUser.id) label="Exams Performed"
) value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
.flatMap((g) => g.participants) color="purple"
.includes(x.id), />
}); <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"
/>
<IconCard
Icon={BsBank}
label="Corporate"
value={masterCorporateUserGroups.length}
color="purple"
onClick={() => setPage("corporate")}
/>
<button
disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")}
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
<BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
</span>
</span>
</button>
</section>
router.push("/list/users"); <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">
: undefined <span className="p-4">Latest students</span>
} <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
user={selectedUser} {users
/> .filter(studentFilter)
</div> .sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))
)} .map((x) => (
</> <UserDisplay key={x.id} {...x} />
</Modal> ))}
{page === "students" && <StudentsList />} </div>
{page === "teachers" && <TeachersList />} </div>
{page === "groups" && <GroupsList />} <div className="bg-white shadow flex flex-col rounded-xl w-full">
{page === "corporate" && <CorporateList />} <span className="p-4">Latest teachers</span>
{page === "" && <DefaultDashboard />} <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>
</>
);
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"
? () => {
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 === "corporate" && <CorporateList />}
{page === "assignments" && <AssignmentsPage />}
{page === "" && <DefaultDashboard />}
</>
);
} }

View File

@@ -2,7 +2,7 @@ import {Assignment} from "@/interfaces/results";
import axios from "axios"; import axios from "axios";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
export default function useAssignments({assigner, assignees}: {assigner?: string; assignees?: string}) { export default function useAssignments({assigner, assignees, corporate}: {assigner?: string; assignees?: string; corporate?: string}) {
const [assignments, setAssignments] = useState<Assignment[]>([]); const [assignments, setAssignments] = useState<Assignment[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
@@ -10,12 +10,13 @@ export default function useAssignments({assigner, assignees}: {assigner?: string
const getData = () => { const getData = () => {
setIsLoading(true); setIsLoading(true);
axios axios
.get<Assignment[]>("/api/assignments") .get<Assignment[]>(!corporate ? "/api/assignments" : `/api/assignments/corporate?id=${corporate}`)
.then((response) => { .then(async (response) => {
if (assigner) { if (assigner) {
setAssignments(response.data.filter((a) => a.assigner === assigner)); setAssignments(response.data.filter((a) => a.assigner === assigner));
return; return;
} }
if (assignees) { if (assignees) {
setAssignments(response.data.filter((a) => a.assignees.filter((x) => assignees.includes(x)).length > 0)); setAssignments(response.data.filter((a) => a.assignees.filter((x) => assignees.includes(x)).length > 0));
return; return;
@@ -26,7 +27,7 @@ export default function useAssignments({assigner, assignees}: {assigner?: string
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}; };
useEffect(getData, [assignees, assigner]); useEffect(getData, [assignees, assigner, corporate]);
return {assignments, isLoading, isError, reload: getData}; return {assignments, isLoading, isError, reload: getData};
} }

View File

@@ -0,0 +1,40 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {uuidv4} from "@firebase/util";
import {Module} from "@/interfaces";
import {getExams} from "@/utils/exams.be";
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
import {capitalize, flatten, uniqBy} from "lodash";
import {User} from "@/interfaces/user";
import moment from "moment";
import {sendEmail} from "@/email";
import {getAllAssignersByCorporate} from "@/utils/groups.be";
import {getAssignmentsByAssigners} from "@/utils/assignments.be";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") return GET(req, res);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query as {id: string};
const assigners = await getAllAssignersByCorporate(id);
const assignments = await getAssignmentsByAssigners([...assigners, id]);
res.status(200).json(assignments);
}

View File

@@ -0,0 +1,14 @@
import {app} from "@/firebase";
import {Assignment} from "@/interfaces/results";
import {collection, getDocs, getFirestore, query, where} from "firebase/firestore";
const db = getFirestore(app);
export const getAssignmentsByAssigner = async (id: string) => {
const {docs} = await getDocs(query(collection(db, "assignments"), where("assigner", "==", id)));
return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
};
export const getAssignmentsByAssigners = async (ids: string[]) => {
return (await Promise.all(ids.map(getAssignmentsByAssigner))).flat();
};

View File

@@ -1,53 +1,51 @@
import { app } from "@/firebase"; import {app} from "@/firebase";
import { CorporateUser, StudentUser, TeacherUser } from "@/interfaces/user"; import {CorporateUser, Group, StudentUser, TeacherUser} from "@/interfaces/user";
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"; import {collection, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore";
import moment from "moment"; import moment from "moment";
import {getUser} from "./users.be";
const db = getFirestore(app); const db = getFirestore(app);
export const updateExpiryDateOnGroup = async ( export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => {
participantID: string, const corporateRef = await getDoc(doc(db, "users", corporateID));
corporateID: string, const participantRef = await getDoc(doc(db, "users", participantID));
) => {
const corporateRef = await getDoc(doc(db, "users", corporateID));
const participantRef = await getDoc(doc(db, "users", participantID));
if (!corporateRef.exists() || !participantRef.exists()) return; if (!corporateRef.exists() || !participantRef.exists()) return;
const corporate = { const corporate = {
...corporateRef.data(), ...corporateRef.data(),
id: corporateRef.id, id: corporateRef.id,
} as CorporateUser; } as CorporateUser;
const participant = { ...participantRef.data(), id: participantRef.id } as const participant = {...participantRef.data(), id: participantRef.id} as StudentUser | TeacherUser;
| StudentUser
| TeacherUser;
if ( if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return;
corporate.type !== "corporate" ||
(participant.type !== "student" && participant.type !== "teacher")
)
return;
if ( if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) {
!corporate.subscriptionExpirationDate || return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: null}, {merge: true});
!participant.subscriptionExpirationDate }
) {
return await setDoc(
doc(db, "users", participant.id),
{ subscriptionExpirationDate: null },
{ merge: true },
);
}
const corporateDate = moment(corporate.subscriptionExpirationDate); const corporateDate = moment(corporate.subscriptionExpirationDate);
const participantDate = moment(participant.subscriptionExpirationDate); const participantDate = moment(participant.subscriptionExpirationDate);
if (corporateDate.isAfter(participantDate)) if (corporateDate.isAfter(participantDate))
return await setDoc( return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: corporateDate.toISOString()}, {merge: true});
doc(db, "users", participant.id),
{ subscriptionExpirationDate: corporateDate.toISOString() },
{ merge: true },
);
return; return;
};
export const getUserGroups = async (id: string): Promise<Group[]> => {
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];
};
export const getAllAssignersByCorporate = async (corporateID: string): Promise<string[]> => {
const groups = await getUserGroups(corporateID);
const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))).flat();
const teacherPromises = await Promise.all(
groupUsers.map(async (u) =>
u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined,
),
);
return teacherPromises.filter((x) => !!x).flat() as string[];
}; };

View File

@@ -1,14 +1,20 @@
import { app } from "@/firebase"; import {app} from "@/firebase";
import { collection, getDocs, getFirestore } from "firebase/firestore"; import {collection, doc, getDoc, getDocs, getFirestore} from "firebase/firestore";
import { User } from "@/interfaces/user"; import {User} from "@/interfaces/user";
const db = getFirestore(app); const db = getFirestore(app);
export async function getUsers() { export async function getUsers() {
const snapshot = await getDocs(collection(db, "users")); const snapshot = await getDocs(collection(db, "users"));
return snapshot.docs.map((doc) => ({ return snapshot.docs.map((doc) => ({
id: doc.id, id: doc.id,
...doc.data(), ...doc.data(),
})) as User[]; })) as User[];
}
export async function getUser(id: string) {
const userDoc = await getDoc(doc(db, "users", id));
return {...userDoc.data(), id} as User;
} }

View File

@@ -25,7 +25,10 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited", expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited",
country: user.demographicInformation?.country || "N/A", country: user.demographicInformation?.country || "N/A",
phone: user.demographicInformation?.phone || "N/A", phone: user.demographicInformation?.phone || "N/A",
employmentPosition: (user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : user.demographicInformation?.employment) || "N/A", employmentPosition:
(user.type === "corporate" || user.type === "mastercorporate"
? user.demographicInformation?.position
: user.demographicInformation?.employment) || "N/A",
gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A", gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A",
verified: user.isVerified?.toString() || "FALSE", verified: user.isVerified?.toString() || "FALSE",
})); }));
@@ -34,3 +37,9 @@ export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group
return `${header}\n${rowsString}`; return `${header}\n${rowsString}`;
}; };
export const getUserName = (user?: User) => {
if (!user) return "N/A";
if (user.type === "corporate" || user.type === "mastercorporate") return user.corporateInformation?.companyInformation?.name || user.name;
return user.name;
};