ENCOA-120: Prevent Assignment Creator from disappearing

This commit is contained in:
Tiago Ribeiro
2024-08-29 12:43:53 +01:00
parent 2fb41f7462
commit c256231cfc
4 changed files with 1728 additions and 2230 deletions

View File

@@ -24,14 +24,13 @@ import useExams from "@/hooks/useExams";
interface Props { interface Props {
isCreating: boolean; isCreating: boolean;
assigner: string;
users: User[]; users: User[];
groups: Group[]; groups: Group[];
assignment?: Assignment; assignment?: Assignment;
cancelCreation: () => void; cancelCreation: () => void;
} }
export default function AssignmentCreator({isCreating, assignment, assigner, groups, users, cancelCreation}: Props) { export default function AssignmentCreator({isCreating, assignment, groups, users, cancelCreation}: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>(assignment?.exams.map((e) => e.module) || []); const [selectedModules, setSelectedModules] = useState<Module[]>(assignment?.exams.map((e) => e.module) || []);
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []); const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
const [name, setName] = useState( const [name, setName] = useState(

View File

@@ -6,7 +6,7 @@ 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, useMemo, useState} from "react";
import { import {
BsArrowLeft, BsArrowLeft,
BsClipboard2Data, BsClipboard2Data,
@@ -29,11 +29,7 @@ import {
} 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 { import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
averageLevelCalculator,
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";
@@ -53,12 +49,7 @@ import { createColumnHelper } from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox"; import Checkbox from "@/components/Low/Checkbox";
import List from "@/components/List"; import List from "@/components/List";
import {getUserCompanyName} from "@/resources/user"; import {getUserCompanyName} from "@/resources/user";
import { import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
futureAssignmentFilter,
pastAssignmentFilter,
archivedAssignmentFilter,
activeAssignmentFilter,
} from "@/utils/assignments";
import useUserBalance from "@/hooks/useUserBalance"; import useUserBalance from "@/hooks/useUserBalance";
interface Props { interface Props {
@@ -66,15 +57,7 @@ interface Props {
} }
type StudentPerformanceItem = User & {corporateName: string; group: string}; type StudentPerformanceItem = User & {corporateName: string; group: string};
const StudentPerformanceList = ({ const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]}) => {
items,
stats,
users,
}: {
items: StudentPerformanceItem[];
stats: Stat[];
users: User[];
}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false); const [isShowingAmount, setIsShowingAmount] = useState(false);
const columnHelper = createColumnHelper<StudentPerformanceItem>(); const columnHelper = createColumnHelper<StudentPerformanceItem>();
@@ -105,81 +88,35 @@ const StudentPerformanceList = ({
cell: (info) => cell: (info) =>
!isShowingAmount !isShowingAmount
? info.getValue() || 0 ? info.getValue() || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.listening", { columnHelper.accessor("levels.listening", {
header: "Listening", header: "Listening",
cell: (info) => cell: (info) =>
!isShowingAmount !isShowingAmount
? info.getValue() || 0 ? info.getValue() || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "listening" &&
x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.writing", { columnHelper.accessor("levels.writing", {
header: "Writing", header: "Writing",
cell: (info) => cell: (info) =>
!isShowingAmount !isShowingAmount
? info.getValue() || 0 ? info.getValue() || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.speaking", { columnHelper.accessor("levels.speaking", {
header: "Speaking", header: "Speaking",
cell: (info) => cell: (info) =>
!isShowingAmount !isShowingAmount
? info.getValue() || 0 ? info.getValue() || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.level", { columnHelper.accessor("levels.level", {
header: "Level", header: "Level",
cell: (info) => cell: (info) =>
!isShowingAmount !isShowingAmount
? info.getValue() || 0 ? info.getValue() || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "level" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels", { columnHelper.accessor("levels", {
id: "overall_level", id: "overall_level",
@@ -188,15 +125,9 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? averageLevelCalculator( ? averageLevelCalculator(
users, users,
stats.filter((x) => x.user === info.row.original.id) stats.filter((x) => x.user === info.row.original.id),
).toFixed(1) ).toFixed(1)
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter((x) => x.user === info.row.original.id)
)
).length
} exams`,
}), }),
]; ];
@@ -210,12 +141,12 @@ const StudentPerformanceList = ({
(a, b) => (a, b) =>
averageLevelCalculator( averageLevelCalculator(
users, users,
stats.filter((x) => x.user === b.id) stats.filter((x) => x.user === b.id),
) - ) -
averageLevelCalculator( averageLevelCalculator(
users, users,
stats.filter((x) => x.user === a.id) stats.filter((x) => x.user === a.id),
) ),
)} )}
columns={columns} columns={columns}
/> />
@@ -227,8 +158,7 @@ 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 [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
@@ -236,16 +166,29 @@ export default function CorporateDashboard({ user }: Props) {
const {users, reload, isLoading} = useUsers(); const {users, reload, isLoading} = useUsers();
const {codes} = useCodes(user.id); const {codes} = useCodes(user.id);
const {groups} = useGroups({admin: user.id}); const {groups} = useGroups({admin: user.id});
const { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
assignments,
isLoading: isAssignmentsLoading,
reload: reloadAssignments,
} = useAssignments({ corporate: user.id });
const {balance} = useUserBalance(); const {balance} = useUserBalance();
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
const assignmentsUsers = useMemo(
() =>
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)),
),
[groups, users, selectedUser],
);
useEffect(() => { useEffect(() => {
setShowModal(!!selectedUser && page === ""); setShowModal(!!selectedUser && page === "");
}, [selectedUser, page]); }, [selectedUser, page]);
@@ -255,26 +198,16 @@ export default function CorporateDashboard({ user }: Props) {
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
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span> <span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span> <span className="text-sm opacity-75">{displayUser.email}</span>
@@ -300,8 +233,7 @@ export default function CorporateDashboard({ user }: Props) {
<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>
@@ -330,8 +262,7 @@ export default function CorporateDashboard({ user }: Props) {
<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>
@@ -343,22 +274,18 @@ export default function CorporateDashboard({ user }: Props) {
}; };
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"> <h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
Groups ({groups.filter(filter).length})
</h2>
</div> </div>
<GroupList user={user} /> <GroupList user={user} />
@@ -380,20 +307,8 @@ export default function CorporateDashboard({ user }: Props) {
/> />
<AssignmentCreator <AssignmentCreator
assignment={selectedAssignment} assignment={selectedAssignment}
groups={groups.filter( groups={assignmentsGroups}
(x) => x.admin === user.id || x.participants.includes(user.id) users={assignmentsUsers}
)}
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} isCreating={isCreatingAssignment}
cancelCreation={() => { cancelCreation={() => {
setIsCreatingAssignment(false); setIsCreatingAssignment(false);
@@ -404,50 +319,31 @@ export default function CorporateDashboard({ user }: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat <BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div> </div>
</div> </div>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeAssignmentFilter).length})</h2>
Active Assignments (
{assignments.filter(activeAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(activeAssignmentFilter).map((a) => ( {assignments.filter(activeAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
))} ))}
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureAssignmentFilter).length})</h2>
Planned Assignments (
{assignments.filter(futureAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<div <div
onClick={() => setIsCreatingAssignment(true)} onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300" className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
>
<BsPlus className="text-6xl" /> <BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span> <span className="text-lg">New Assignment</span>
</div> </div>
@@ -465,9 +361,7 @@ export default function CorporateDashboard({ user }: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastAssignmentFilter).length})</h2>
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(pastAssignmentFilter).map((a) => ( {assignments.filter(pastAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -484,10 +378,7 @@ export default function CorporateDashboard({ user }: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})</h2>
Archived Assignments (
{assignments.filter(archivedAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(archivedAssignmentFilter).map((a) => ( {assignments.filter(archivedAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -509,11 +400,7 @@ export default function CorporateDashboard({ user }: Props) {
const StudentPerformancePage = () => { const StudentPerformancePage = () => {
const students = users const students = users
.filter( .filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
(x) =>
x.type === "student" &&
groups.flatMap((g) => g.participants).includes(x.id)
)
.map((u) => ({ .map((u) => ({
...u, ...u,
group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A", group: groups.find((x) => x.participants.includes(u.id))?.name || "N/A",
@@ -525,19 +412,15 @@ export default function CorporateDashboard({ user }: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reload} onClick={reload}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat <BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
className={clsx("text-xl", isLoading && "animate-spin")}
/>
</div> </div>
</div> </div>
<StudentPerformanceList items={students} stats={stats} users={users} /> <StudentPerformanceList items={students} stats={stats} users={users} />
@@ -555,12 +438,7 @@ export default function CorporateDashboard({ user }: Props) {
.filter((f) => !!f.focus); .filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({ const bandScores = formattedStats.map((s) => ({
module: s.module, module: s.module,
level: calculateBandScore( level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
s.score.correct,
s.score.total,
s.module,
s.focus!
),
})); }));
const levels: {[key in Module]: number} = { const levels: {[key in Module]: number} = {
@@ -579,11 +457,7 @@ export default function CorporateDashboard({ user }: Props) {
<> <>
{corporateUserToShow && ( {corporateUserToShow && (
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1"> <div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
Linked to:{" "} Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
<b>
{corporateUserToShow?.corporateInformation?.companyInformation
.name || corporateUserToShow.name}
</b>
</div> </div>
)} )}
<section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center"> <section className="grid grid-cols-5 -md:grid-cols-2 gap-4 text-center">
@@ -604,46 +478,26 @@ export default function CorporateDashboard({ user }: Props) {
<IconCard <IconCard
Icon={BsClipboard2Data} Icon={BsClipboard2Data}
label="Exams Performed" label="Exams Performed"
value={ value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
color="purple" color="purple"
/> />
<IconCard <IconCard
Icon={BsPaperclip} Icon={BsPaperclip}
label="Average Level" label="Average Level"
value={averageLevelCalculator( value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
)
).toFixed(1)}
color="purple"
/>
<IconCard
onClick={() => setPage("groups")}
Icon={BsPeople}
label="Groups"
value={groups.length}
color="purple" color="purple"
/> />
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
<IconCard <IconCard
Icon={BsPersonCheck} Icon={BsPersonCheck}
label="User Balance" label="User Balance"
value={`${balance}/${ value={`${balance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
color="purple" color="purple"
/> />
<IconCard <IconCard
Icon={BsClock} Icon={BsClock}
label="Expiration Date" label="Expiration Date"
value={ value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
color="rose" color="rose"
/> />
<IconCard <IconCard
@@ -656,15 +510,12 @@ export default function CorporateDashboard({ user }: Props) {
<button <button
disabled={isAssignmentsLoading} disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")} 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" 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" /> <BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl"> <span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span> <span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light"> <span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading {isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
? "Loading..."
: assignments.filter((a) => !a.archived).length}
</span> </span>
</span> </span>
</button> </button>
@@ -698,11 +549,7 @@ export default function CorporateDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(studentFilter) .filter(studentFilter)
.sort( .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
))} ))}
@@ -715,8 +562,7 @@ export default function CorporateDashboard({ user }: Props) {
.filter(studentFilter) .filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
Object.keys(groupByExam(getStatsByStudent(a))).length
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -740,8 +586,7 @@ export default function CorporateDashboard({ user }: Props) {
if (shouldReload) reload(); if (shouldReload) reload();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "teacher"
selectedUser.type === "teacher"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-students", id: "view-students",
@@ -751,11 +596,7 @@ export default function CorporateDashboard({ user }: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });
@@ -765,8 +606,7 @@ export default function CorporateDashboard({ user }: Props) {
: undefined : undefined
} }
onViewTeachers={ onViewTeachers={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "student"
selectedUser.type === "student"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-teachers", id: "view-teachers",
@@ -776,11 +616,7 @@ export default function CorporateDashboard({ user }: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });

View File

@@ -2,13 +2,7 @@
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import { import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user";
CorporateUser,
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";
@@ -33,11 +27,7 @@ import {
import UserCard from "@/components/UserCard"; import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import { import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
averageLevelCalculator,
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";
@@ -60,18 +50,9 @@ import Checkbox from "@/components/Low/Checkbox";
import {groupBy, uniq, uniqBy} from "lodash"; import {groupBy, uniq, uniqBy} from "lodash";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import {Menu, MenuButton, MenuItem, MenuItems} from "@headlessui/react"; import {Menu, MenuButton, MenuItem, MenuItems} from "@headlessui/react";
import { import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import MasterStatistical from "./MasterStatistical"; import MasterStatistical from "./MasterStatistical";
import { import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
futureAssignmentFilter,
pastAssignmentFilter,
archivedAssignmentFilter,
activeAssignmentFilter,
} from "@/utils/assignments";
import useUserBalance from "@/hooks/useUserBalance"; import useUserBalance from "@/hooks/useUserBalance";
interface Props { interface Props {
@@ -82,37 +63,23 @@ type StudentPerformanceItem = User & {
corporate?: CorporateUser; corporate?: CorporateUser;
group?: Group; group?: Group;
}; };
const StudentPerformanceList = ({ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => {
items,
stats,
users,
groups,
}: {
items: StudentPerformanceItem[];
stats: Stat[];
users: User[];
groups: Group[];
}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false); const [isShowingAmount, setIsShowingAmount] = useState(false);
const [availableCorporates] = useState( const [availableCorporates] = useState(
uniqBy( uniqBy(
items.map((x) => x.corporate), items.map((x) => x.corporate),
"id" "id",
) ),
); );
const [availableGroups] = useState( const [availableGroups] = useState(
uniqBy( uniqBy(
items.map((x) => x.group), items.map((x) => x.group),
"id" "id",
) ),
); );
const [selectedCorporate, setSelectedCorporate] = useState< const [selectedCorporate, setSelectedCorporate] = useState<CorporateUser | null | undefined>(null);
CorporateUser | null | undefined const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(null);
>(null);
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(
null
);
const columnHelper = createColumnHelper<StudentPerformanceItem>(); const columnHelper = createColumnHelper<StudentPerformanceItem>();
@@ -135,10 +102,7 @@ const StudentPerformanceList = ({
}), }),
columnHelper.accessor("corporate", { columnHelper.accessor("corporate", {
header: "Corporate", header: "Corporate",
cell: (info) => cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"),
!!info.getValue()
? getUserCompanyName(info.getValue() as User, users, groups)
: "N/A",
}), }),
columnHelper.accessor("levels.reading", { columnHelper.accessor("levels.reading", {
header: "Reading", header: "Reading",
@@ -146,30 +110,15 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter( .filter((x) => x.module === "reading" && x.user === info.row.original.id)
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter( .filter((x) => x.module === "reading" && x.user === info.row.original.id)
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic" info.row.original.focus || "academic",
) || 0 ) || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.listening", { columnHelper.accessor("levels.listening", {
header: "Listening", header: "Listening",
@@ -177,31 +126,15 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter( .filter((x) => x.module === "listening" && x.user === info.row.original.id)
(x) =>
x.module === "listening" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter( .filter((x) => x.module === "listening" && x.user === info.row.original.id)
(x) =>
x.module === "listening" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic" info.row.original.focus || "academic",
) || 0 ) || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "listening" &&
x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.writing", { columnHelper.accessor("levels.writing", {
header: "Writing", header: "Writing",
@@ -209,30 +142,15 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter( .filter((x) => x.module === "writing" && x.user === info.row.original.id)
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter( .filter((x) => x.module === "writing" && x.user === info.row.original.id)
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic" info.row.original.focus || "academic",
) || 0 ) || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.speaking", { columnHelper.accessor("levels.speaking", {
header: "Speaking", header: "Speaking",
@@ -240,30 +158,15 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter( .filter((x) => x.module === "speaking" && x.user === info.row.original.id)
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter( .filter((x) => x.module === "speaking" && x.user === info.row.original.id)
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic" info.row.original.focus || "academic",
) || 0 ) || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.level", { columnHelper.accessor("levels.level", {
header: "Level", header: "Level",
@@ -271,28 +174,15 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter( .filter((x) => x.module === "level" && x.user === info.row.original.id)
(x) => x.module === "level" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter( .filter((x) => x.module === "level" && x.user === info.row.original.id)
(x) => x.module === "level" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic" info.row.original.focus || "academic",
) || 0 ) || 0
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "level" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels", { columnHelper.accessor("levels", {
id: "overall_level", id: "overall_level",
@@ -301,24 +191,16 @@ const StudentPerformanceList = ({
!isShowingAmount !isShowingAmount
? averageLevelCalculator( ? averageLevelCalculator(
users, users,
stats.filter((x) => x.user === info.row.original.id) stats.filter((x) => x.user === info.row.original.id),
).toFixed(1) ).toFixed(1)
: `${ : `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
Object.keys(
groupByExam(
stats.filter((x) => x.user === info.row.original.id)
)
).length
} exams`,
}), }),
]; ];
const filterUsers = (data: StudentPerformanceItem[]) => { const filterUsers = (data: StudentPerformanceItem[]) => {
console.log(data, selectedCorporate); console.log(data, selectedCorporate);
const filterByCorporate = (item: StudentPerformanceItem) => const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id;
item.corporate?.id === selectedCorporate?.id; const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id;
const filterByGroup = (item: StudentPerformanceItem) =>
item.group?.id === selectedGroup?.id;
const filters: ((item: StudentPerformanceItem) => boolean)[] = []; const filters: ((item: StudentPerformanceItem) => boolean)[] = [];
if (selectedCorporate !== null) filters.push(filterByCorporate); if (selectedCorporate !== null) filters.push(filterByCorporate);
@@ -345,10 +227,7 @@ const StudentPerformanceList = ({
<Select <Select
options={availableCorporates.map((x) => ({ options={availableCorporates.map((x) => ({
value: x?.id || "N/A", value: x?.id || "N/A",
label: label: x?.corporateInformation?.companyInformation?.name || x?.name || "N/A",
x?.corporateInformation?.companyInformation?.name ||
x?.name ||
"N/A",
}))} }))}
isClearable isClearable
value={ value={
@@ -357,8 +236,7 @@ const StudentPerformanceList = ({
: { : {
value: selectedCorporate?.id || "N/A", value: selectedCorporate?.id || "N/A",
label: label:
selectedCorporate?.corporateInformation selectedCorporate?.corporateInformation?.companyInformation?.name ||
?.companyInformation?.name ||
selectedCorporate?.name || selectedCorporate?.name ||
"N/A", "N/A",
} }
@@ -368,11 +246,7 @@ const StudentPerformanceList = ({
!value !value
? setSelectedCorporate(null) ? setSelectedCorporate(null)
: setSelectedCorporate( : setSelectedCorporate(
value.value === "N/A" value.value === "N/A" ? undefined : availableCorporates.find((x) => x?.id === value.value),
? undefined
: availableCorporates.find(
(x) => x?.id === value.value
)
) )
} }
/> />
@@ -394,11 +268,7 @@ const StudentPerformanceList = ({
onChange={(value) => onChange={(value) =>
!value !value
? setSelectedGroup(null) ? setSelectedGroup(null)
: setSelectedGroup( : setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value))
value.value === "N/A"
? undefined
: availableGroups.find((x) => x?.id === value.value)
)
} }
/> />
</div> </div>
@@ -411,13 +281,13 @@ const StudentPerformanceList = ({
(a, b) => (a, b) =>
averageLevelCalculator( averageLevelCalculator(
users, users,
stats.filter((x) => x.user === b.id) stats.filter((x) => x.user === b.id),
) - ) -
averageLevelCalculator( averageLevelCalculator(
users, users,
stats.filter((x) => x.user === a.id) stats.filter((x) => x.user === a.id),
) ),
) ),
)} )}
columns={columns} columns={columns}
/> />
@@ -431,30 +301,37 @@ export default function MasterCorporateDashboard({ user }: Props) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>(); const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [corporateAssignments, setCorporateAssignments] = useState< const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]);
(Assignment & { corporate?: CorporateUser })[]
>([]);
const {data: stats} = useFilterRecordsByUser<Stat[]>(); const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {groups} = useGroups({admin: user.id, userType: user.type}); const {groups} = useGroups({admin: user.id, userType: user.type});
const {balance} = useUserBalance(); const {balance} = useUserBalance();
const masterCorporateUserGroups = useMemo(() => [ const masterCorporateUserGroups = useMemo(
...new Set( () => [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))],
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants) [groups, user.id],
);
const corporateUserGroups = useMemo(() => [...new Set(groups.flatMap((g) => g.participants))], [groups]);
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
const assignmentsUsers = useMemo(
() =>
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)),
), ),
], [groups, user.id]); [groups, users, selectedUser],
);
const corporateUserGroups = useMemo(() => [
...new Set(groups.flatMap((g) => g.participants)),
], [groups])
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();
@@ -468,33 +345,21 @@ export default function MasterCorporateDashboard({ user }: Props) {
assignments.filter(activeAssignmentFilter).map((a) => ({ assignments.filter(activeAssignmentFilter).map((a) => ({
...a, ...a,
corporate: !!users.find((x) => x.id === a.assigner) corporate: !!users.find((x) => x.id === a.assigner)
? getCorporateUser( ? getCorporateUser(users.find((x) => x.id === a.assigner)!, users, groups)
users.find((x) => x.id === a.assigner)!,
users,
groups
)
: undefined, : undefined,
})) })),
); );
}, [assignments, groups, users]); }, [assignments, groups, users]);
const studentFilter = (user: User) => const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
user.type === "student" && corporateUserGroups.includes(user.id); const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
const teacherFilter = (user: User) => const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
user.type === "teacher" && corporateUserGroups.includes(user.id);
const getStatsByStudent = (user: User) =>
stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
<div <div
onClick={() => setSelectedUser(displayUser)} onClick={() => setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300" className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
> <img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span> <span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span> <span className="text-sm opacity-75">{displayUser.email}</span>
@@ -504,10 +369,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -517,8 +379,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<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>
@@ -531,10 +392,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
const TeachersList = () => { const TeachersList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "teacher" && x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -544,8 +402,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<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>
@@ -557,10 +414,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
}; };
const corporateUserFilter = (x: User) => const corporateUserFilter = (x: User) =>
x.type === "corporate" && x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
(!!selectedUser
? masterCorporateUserGroups.includes(x.id) || false
: masterCorporateUserGroups.includes(x.id));
const CorporateList = () => { const CorporateList = () => {
return ( return (
@@ -571,8 +425,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<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>
@@ -589,8 +442,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<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>
@@ -604,11 +456,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
const StudentPerformancePage = () => { const StudentPerformancePage = () => {
const students = users const students = users
.filter( .filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
(x) =>
x.type === "student" &&
groups.flatMap((g) => g.participants).includes(x.id)
)
.map((u) => ({ .map((u) => ({
...u, ...u,
group: groups.find((x) => x.participants.includes(u.id)), group: groups.find((x) => x.participants.includes(u.id)),
@@ -620,30 +468,18 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat <BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div> </div>
</div> </div>
<StudentPerformanceList <StudentPerformanceList items={students} stats={stats} users={users} groups={groups} />
items={students}
stats={stats}
users={users}
groups={groups}
/>
</> </>
); );
}; };
@@ -662,20 +498,8 @@ export default function MasterCorporateDashboard({ user }: Props) {
/> />
<AssignmentCreator <AssignmentCreator
assignment={selectedAssignment} assignment={selectedAssignment}
groups={groups.filter( groups={assignmentsGroups}
(x) => x.admin === user.id || x.participants.includes(user.id) users={assignmentsUsers}
)}
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} isCreating={isCreatingAssignment}
cancelCreation={() => { cancelCreation={() => {
setIsCreatingAssignment(false); setIsCreatingAssignment(false);
@@ -686,88 +510,49 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat <BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-lg font-bold">Active Assignments Status</span> <span className="text-lg font-bold">Active Assignments Status</span>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span> <span>
<b>Total:</b>{" "} <b>Total:</b> {assignments.filter(activeAssignmentFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/
{assignments {assignments.filter(activeAssignmentFilter).reduce((acc, curr) => curr.exams.length + acc, 0)}
.filter(activeAssignmentFilter)
.reduce((acc, curr) => acc + curr.results.length, 0)}
/
{assignments
.filter(activeAssignmentFilter)
.reduce((acc, curr) => curr.exams.length + acc, 0)}
</span> </span>
{Object.keys( {Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => (
groupBy(corporateAssignments, (x) => x.corporate?.id)
).map((x) => (
<div key={x}> <div key={x}>
<span className="font-semibold"> <span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span>
{getUserCompanyName(
users.find((u) => u.id === x)!,
users,
groups
)}
:{" "}
</span>
<span> <span>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[ {groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/
x {groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)}
].reduce((acc, curr) => curr.results.length + acc, 0)}
/
{groupBy(corporateAssignments, (x) => x.corporate?.id)[
x
].reduce((acc, curr) => curr.exams.length + acc, 0)}
</span> </span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeAssignmentFilter).length})</h2>
Active Assignments (
{assignments.filter(activeAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(activeAssignmentFilter).map((a) => ( {assignments.filter(activeAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
))} ))}
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureAssignmentFilter).length})</h2>
Planned Assignments (
{assignments.filter(futureAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<div <div
onClick={() => setIsCreatingAssignment(true)} onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300" className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
>
<BsPlus className="text-6xl" /> <BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span> <span className="text-lg">New Assignment</span>
</div> </div>
@@ -785,9 +570,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastAssignmentFilter).length})</h2>
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(pastAssignmentFilter).map((a) => ( {assignments.filter(pastAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -804,10 +587,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})</h2>
Archived Assignments (
{assignments.filter(archivedAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(archivedAssignmentFilter).map((a) => ( {assignments.filter(archivedAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -834,7 +614,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
if (user) return [...accm, user]; if (user) return [...accm, user];
return accm; return accm;
}, []), }, []),
[masterCorporateUserGroups, users] [masterCorporateUserGroups, users],
); );
const MasterStatisticalPage = () => { const MasterStatisticalPage = () => {
@@ -843,17 +623,13 @@ export default function MasterCorporateDashboard({ user }: Props) {
<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">Master Statistical</h2> <h2 className="text-2xl font-semibold">Master Statistical</h2>
</div> </div>
<MasterStatistical <MasterStatistical users={users} corporateUsers={masterCorporateUsers} />
users={users}
corporateUsers={masterCorporateUsers}
/>
</> </>
); );
}; };
@@ -878,11 +654,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<IconCard <IconCard
Icon={BsClipboard2Data} Icon={BsClipboard2Data}
label="Exams Performed" label="Exams Performed"
value={ value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
color="purple" color="purple"
/> />
<IconCard <IconCard
@@ -890,35 +662,21 @@ export default function MasterCorporateDashboard({ user }: Props) {
label="Average Level" label="Average Level"
value={averageLevelCalculator( value={averageLevelCalculator(
users, users,
stats.filter((s) => stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)),
groups.flatMap((g) => g.participants).includes(s.user)
)
).toFixed(1)} ).toFixed(1)}
color="purple" color="purple"
/> />
<IconCard <IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" />
onClick={() => setPage("groups")}
Icon={BsPeople}
label="Groups"
value={groups.length}
color="purple"
/>
<IconCard <IconCard
Icon={BsPersonCheck} Icon={BsPersonCheck}
label="User Balance" label="User Balance"
value={`${balance}/${ value={`${balance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
color="purple" color="purple"
/> />
<IconCard <IconCard
Icon={BsClock} Icon={BsClock}
label="Expiration Date" label="Expiration Date"
value={ value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
color="rose" color="rose"
/> />
<IconCard <IconCard
@@ -945,15 +703,12 @@ export default function MasterCorporateDashboard({ user }: Props) {
<button <button
disabled={isAssignmentsLoading} disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")} 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" 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" /> <BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl"> <span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span> <span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light"> <span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading {isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length}
? "Loading..."
: assignments.filter((a) => !a.archived).length}
</span> </span>
</span> </span>
</button> </button>
@@ -987,11 +742,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(studentFilter) .filter(studentFilter)
.sort( .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
))} ))}
@@ -1004,8 +755,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
.filter(studentFilter) .filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
Object.keys(groupByExam(getStatsByStudent(a))).length
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -1025,8 +775,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
<UserCard <UserCard
maxUserAmount={ maxUserAmount={
user.type === "mastercorporate" user.type === "mastercorporate"
? (user.corporateInformation?.companyInformation ? (user.corporateInformation?.companyInformation?.userAmount || 0) - balance
?.userAmount || 0) - balance
: undefined : undefined
} }
loggedInUser={user} loggedInUser={user}
@@ -1035,8 +784,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
if (shouldReload) reload(); if (shouldReload) reload();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "teacher"
selectedUser.type === "teacher"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-students", id: "view-students",
@@ -1046,11 +794,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });
@@ -1060,8 +804,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
: undefined : undefined
} }
onViewTeachers={ onViewTeachers={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "student"
selectedUser.type === "student"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-teachers", id: "view-teachers",
@@ -1071,11 +814,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter( .filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });

View File

@@ -6,7 +6,7 @@ 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, useMemo, useState} from "react";
import { import {
BsArrowLeft, BsArrowLeft,
BsArrowRepeat, BsArrowRepeat,
@@ -48,12 +48,7 @@ import AssignmentView from "./AssignmentView";
import {getUserCorporate} from "@/utils/groups"; import {getUserCorporate} from "@/utils/groups";
import {checkAccess} from "@/utils/permissions"; import {checkAccess} from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
futureAssignmentFilter,
pastAssignmentFilter,
archivedAssignmentFilter,
activeAssignmentFilter
} from '@/utils/assignments';
interface Props { interface Props {
user: User; user: User;
@@ -65,18 +60,30 @@ export default function TeacherDashboard({ user }: Props) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>(); const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [corporateUserToShow, setCorporateUserToShow] = const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
useState<CorporateUser>();
const {data: stats} = useFilterRecordsByUser<Stat[]>(); const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users, reload} = useUsers(); const {users, reload} = useUsers();
const {groups} = useGroups({adminAdmins: user.id}); const {groups} = useGroups({adminAdmins: user.id});
const {permissions} = usePermissions(user.id); const {permissions} = usePermissions(user.id);
const { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
assignments,
isLoading: isAssignmentsLoading, const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
reload: reloadAssignments,
} = useAssignments({ assigner: user.id }); const assignmentsUsers = useMemo(
() =>
users.filter(
(x) =>
x.type === "student" &&
(!!selectedUser
? groups
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id)
: groups.flatMap((g) => g.participants).includes(x.id)),
),
[groups, users, selectedUser],
);
useEffect(() => { useEffect(() => {
setShowModal(!!selectedUser && page === ""); setShowModal(!!selectedUser && page === "");
@@ -86,23 +93,15 @@ export default function TeacherDashboard({ user }: Props) {
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" &&
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
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span> <span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span> <span className="text-sm opacity-75">{displayUser.email}</span>
@@ -128,8 +127,7 @@ export default function TeacherDashboard({ user }: Props) {
<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>
@@ -148,14 +146,11 @@ export default function TeacherDashboard({ user }: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Groups ({groups.filter(filter).length})</h2>
Groups ({groups.filter(filter).length})
</h2>
</div> </div>
<GroupList user={user} /> <GroupList user={user} />
@@ -173,12 +168,7 @@ export default function TeacherDashboard({ user }: Props) {
.filter((f) => !!f.focus); .filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({ const bandScores = formattedStats.map((s) => ({
module: s.module, module: s.module,
level: calculateBandScore( level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
s.score.correct,
s.score.total,
s.module,
s.focus!
),
})); }));
const levels: {[key in Module]: number} = { const levels: {[key in Module]: number} = {
@@ -207,20 +197,8 @@ export default function TeacherDashboard({ user }: Props) {
/> />
<AssignmentCreator <AssignmentCreator
assignment={selectedAssignment} assignment={selectedAssignment}
groups={groups.filter( groups={assignmentsGroups}
(x) => x.admin === user.id || x.participants.includes(user.id) users={assignmentsUsers}
)}
users={users.filter(
(x) =>
x.type === "student" &&
(!!selectedUser
? groups
.filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants)
.includes(x.id)
: groups.flatMap((g) => g.participants).includes(x.id))
)}
assigner={user.id}
isCreating={isCreatingAssignment} isCreating={isCreatingAssignment}
cancelCreation={() => { cancelCreation={() => {
setIsCreatingAssignment(false); setIsCreatingAssignment(false);
@@ -231,48 +209,31 @@ export default function TeacherDashboard({ user }: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300" className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat <BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div> </div>
</div> </div>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeAssignmentFilter).length})</h2>
Active Assignments ({assignments.filter(activeAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(activeAssignmentFilter).map((a) => ( {assignments.filter(activeAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
))} ))}
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureAssignmentFilter).length})</h2>
Planned Assignments ({assignments.filter(futureAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<div <div
onClick={() => setIsCreatingAssignment(true)} onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300" className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
>
<BsPlus className="text-6xl" /> <BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span> <span className="text-lg">New Assignment</span>
</div> </div>
@@ -290,9 +251,7 @@ export default function TeacherDashboard({ user }: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastAssignmentFilter).length})</h2>
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(pastAssignmentFilter).map((a) => ( {assignments.filter(pastAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -309,9 +268,7 @@ export default function TeacherDashboard({ user }: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold"> <h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})</h2>
Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(archivedAssignmentFilter).map((a) => ( {assignments.filter(archivedAssignmentFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -335,19 +292,14 @@ export default function TeacherDashboard({ user }: Props) {
<> <>
{corporateUserToShow && ( {corporateUserToShow && (
<div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1"> <div className="absolute top-4 right-4 bg-neutral-200 px-2 rounded-lg py-1">
Linked to:{" "} Linked to: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
<b>
{corporateUserToShow?.corporateInformation?.companyInformation
.name || corporateUserToShow.name}
</b>
</div> </div>
)} )}
<section <section
className={clsx( className={clsx(
"flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center", "flex -lg:flex-wrap gap-4 items-center -lg:justify-center lg:justify-start text-center",
!!corporateUserToShow && "mt-12 xl:mt-6" !!corporateUserToShow && "mt-12 xl:mt-6",
)} )}>
>
<IconCard <IconCard
onClick={() => setPage("students")} onClick={() => setPage("students")}
Icon={BsPersonFill} Icon={BsPersonFill}
@@ -358,29 +310,16 @@ export default function TeacherDashboard({ user }: Props) {
<IconCard <IconCard
Icon={BsClipboard2Data} Icon={BsClipboard2Data}
label="Exams Performed" label="Exams Performed"
value={ value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
color="purple" color="purple"
/> />
<IconCard <IconCard
Icon={BsPaperclip} Icon={BsPaperclip}
label="Average Level" label="Average Level"
value={averageLevelCalculator( value={averageLevelCalculator(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
)
).toFixed(1)}
color="purple" color="purple"
/> />
{checkAccess( {checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
user,
["teacher", "developer"],
permissions,
"viewGroup"
) && (
<IconCard <IconCard
Icon={BsPeople} Icon={BsPeople}
label="Groups" label="Groups"
@@ -391,14 +330,11 @@ export default function TeacherDashboard({ user }: Props) {
)} )}
<div <div
onClick={() => setPage("assignments")} onClick={() => setPage("assignments")}
className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300" className="bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300">
>
<BsEnvelopePaper className="text-6xl text-mti-purple-light" /> <BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl"> <span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span> <span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light"> <span className="font-semibold text-mti-purple-light">{assignments.filter((a) => !a.archived).length}</span>
{assignments.filter((a) => !a.archived).length}
</span>
</span> </span>
</div> </div>
</section> </section>
@@ -420,11 +356,7 @@ export default function TeacherDashboard({ user }: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(studentFilter) .filter(studentFilter)
.sort( .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
))} ))}
@@ -437,8 +369,7 @@ export default function TeacherDashboard({ user }: Props) {
.filter(studentFilter) .filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length,
Object.keys(groupByExam(getStatsByStudent(a))).length
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -462,16 +393,9 @@ export default function TeacherDashboard({ user }: Props) {
if (shouldReload) reload(); if (shouldReload) reload();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
selectedUser.type === "teacher"
? () => setPage("students")
: undefined
}
onViewTeachers={
selectedUser.type === "corporate"
? () => setPage("teachers")
: undefined
} }
onViewTeachers={selectedUser.type === "corporate" ? () => setPage("teachers") : undefined}
user={selectedUser} user={selectedUser}
/> />
</div> </div>