Merged in features-21-08-24 (pull request #79)
ENCOA: 86 + 101 + 102 Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -178,6 +178,15 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldRenderPDFIcon = () => {
|
||||||
|
if(assignment) {
|
||||||
|
return assignment.released;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
||||||
@@ -202,7 +211,7 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
Level{" "}
|
Level{" "}
|
||||||
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
|
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
{renderPdfIcon(session, textColor, textColor)}
|
{shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)}
|
||||||
</div>
|
</div>
|
||||||
{examNumber === undefined ? (
|
{examNumber === undefined ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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 {useAssignmentRelease} from "@/hooks/useAssignmentRelease";
|
||||||
import {getUserName} from "@/utils/users";
|
import {getUserName} from "@/utils/users";
|
||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
|
|
||||||
@@ -40,11 +41,14 @@ export default function AssignmentCard({
|
|||||||
allowUnarchive,
|
allowUnarchive,
|
||||||
allowExcelDownload,
|
allowExcelDownload,
|
||||||
users,
|
users,
|
||||||
|
released,
|
||||||
}: Assignment & Props) {
|
}: Assignment & Props) {
|
||||||
const renderPdfIcon = usePDFDownload("assignments");
|
const renderPdfIcon = usePDFDownload("assignments");
|
||||||
const renderExcelIcon = usePDFDownload("assignments", "excel");
|
const renderExcelIcon = usePDFDownload("assignments", "excel");
|
||||||
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
const renderArchiveIcon = useAssignmentArchive(id, reload);
|
||||||
const renderUnarchiveIcon = useAssignmentUnarchive(id, reload);
|
const renderUnarchiveIcon = useAssignmentUnarchive(id, reload);
|
||||||
|
const renderReleaseIcon = useAssignmentRelease(id, reload);
|
||||||
|
|
||||||
|
|
||||||
const calculateAverageModuleScore = (module: Module) => {
|
const calculateAverageModuleScore = (module: Module) => {
|
||||||
const resultModuleBandScores = results.map((r) => {
|
const resultModuleBandScores = results.map((r) => {
|
||||||
@@ -66,10 +70,11 @@ export default function AssignmentCard({
|
|||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<h3 className="text-xl font-semibold">{name}</h3>
|
<h3 className="text-xl font-semibold">{name}</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{allowDownload && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowDownload && released && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowExcelDownload && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowExcelDownload && released && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
{allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
{allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
|
{!released && renderReleaseIcon("text-mti-gray-dim", "text-mti-gray-dim")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
import {
|
||||||
import {generate} from "random-words";
|
BsBook,
|
||||||
import {capitalize} from "lodash";
|
BsCheckCircle,
|
||||||
|
BsClipboard,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
BsXCircle,
|
||||||
|
} from "react-icons/bs";
|
||||||
|
import { generate } from "random-words";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {Group, User} from "@/interfaces/user";
|
import { Group, User } from "@/interfaces/user";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import {calculateAverageLevel} from "@/utils/score";
|
import { calculateAverageLevel } from "@/utils/score";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {getExam} from "@/utils/exams";
|
import { getExam } from "@/utils/exams";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import {InstructorGender, Variant} from "@/interfaces/exam";
|
import { InstructorGender, Variant } from "@/interfaces/exam";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import useExams from "@/hooks/useExams";
|
import useExams from "@/hooks/useExams";
|
||||||
|
|
||||||
@@ -31,41 +39,78 @@ interface Props {
|
|||||||
cancelCreation: () => void;
|
cancelCreation: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssignmentCreator({isCreating, assignment, assigner, groups, users, cancelCreation}: Props) {
|
export default function AssignmentCreator({
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>(assignment?.exams.map((e) => e.module) || []);
|
isCreating,
|
||||||
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
|
assignment,
|
||||||
const [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize}));
|
assigner,
|
||||||
|
groups,
|
||||||
|
users,
|
||||||
|
cancelCreation,
|
||||||
|
}: Props) {
|
||||||
|
const [selectedModules, setSelectedModules] = useState<Module[]>(
|
||||||
|
assignment?.exams.map((e) => e.module) || []
|
||||||
|
);
|
||||||
|
const [assignees, setAssignees] = useState<string[]>(
|
||||||
|
assignment?.assignees || []
|
||||||
|
);
|
||||||
|
const [name, setName] = useState(
|
||||||
|
assignment?.name ||
|
||||||
|
generate({
|
||||||
|
minLength: 6,
|
||||||
|
maxLength: 8,
|
||||||
|
min: 2,
|
||||||
|
max: 3,
|
||||||
|
join: " ",
|
||||||
|
formatter: capitalize,
|
||||||
|
})
|
||||||
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : new Date());
|
const [startDate, setStartDate] = useState<Date | null>(
|
||||||
|
assignment ? moment(assignment.startDate).toDate() : new Date()
|
||||||
|
);
|
||||||
const [endDate, setEndDate] = useState<Date | null>(
|
const [endDate, setEndDate] = useState<Date | null>(
|
||||||
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
|
assignment
|
||||||
|
? moment(assignment.endDate).toDate()
|
||||||
|
: moment().hours(23).minutes(59).add(8, "day").toDate()
|
||||||
);
|
);
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
const [instructorGender, setInstructorGender] = useState<InstructorGender>(assignment?.instructorGender || "varied");
|
const [instructorGender, setInstructorGender] = useState<InstructorGender>(
|
||||||
|
assignment?.instructorGender || "varied"
|
||||||
|
);
|
||||||
// creates a new exam for each assignee or just one exam for all assignees
|
// creates a new exam for each assignee or just one exam for all assignees
|
||||||
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
||||||
const [useRandomExams, setUseRandomExams] = useState(true);
|
const [useRandomExams, setUseRandomExams] = useState(true);
|
||||||
const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]);
|
const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]);
|
||||||
|
|
||||||
const {exams} = useExams();
|
const { exams } = useExams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
|
setExamIDs((prev) =>
|
||||||
|
prev.filter((x) => selectedModules.includes(x.module))
|
||||||
|
);
|
||||||
}, [selectedModules]);
|
}, [selectedModules]);
|
||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
setSelectedModules((prev) =>
|
||||||
|
prev.includes(module) ? modules : [...modules, module]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAssignee = (user: User) => {
|
const toggleAssignee = (user: User) => {
|
||||||
setAssignees((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
|
setAssignees((prev) =>
|
||||||
|
prev.includes(user.id)
|
||||||
|
? prev.filter((a) => a !== user.id)
|
||||||
|
: [...prev, user.id]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAssignment = () => {
|
const createAssignment = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
(assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, {
|
(assignment ? axios.patch : axios.post)(
|
||||||
|
`/api/assignments${assignment ? `/${assignment.id}` : ""}`,
|
||||||
|
{
|
||||||
assignees,
|
assignees,
|
||||||
name,
|
name,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -75,9 +120,14 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
generateMultiple,
|
generateMultiple,
|
||||||
variant,
|
variant,
|
||||||
instructorGender,
|
instructorGender,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been ${
|
||||||
|
assignment ? "updated" : "created"
|
||||||
|
} successfully!`
|
||||||
|
);
|
||||||
cancelCreation();
|
cancelCreation();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -91,11 +141,18 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
if (assignment) {
|
if (assignment) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!confirm(`Are you sure you want to delete the "${assignment.name}" assignment?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to delete the "${assignment.name}" assignment?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
axios
|
axios
|
||||||
.delete(`api/assignments/${assignment.id}`)
|
.delete(`api/assignments/${assignment.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been deleted successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been deleted successfully!`
|
||||||
|
);
|
||||||
cancelCreation();
|
cancelCreation();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -106,111 +163,199 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startAssignment = () => {
|
||||||
|
if (assignment) {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(`/api/assignments/${assignment.id}/start`)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`The assignment "${name}" has been started successfully!`
|
||||||
|
);
|
||||||
|
cancelCreation();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
toast.error("Something went wrong, please try again later!");
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isCreating} onClose={cancelCreation} title="New Assignment">
|
<Modal isOpen={isCreating} onClose={cancelCreation} title="New Assignment">
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
<section className="w-full grid -md:grid-cols-1 md:grid-cols-2 place-items-center lg:grid-cols-6 -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
<section className="w-full grid -md:grid-cols-1 md:grid-cols-2 place-items-center lg:grid-cols-6 -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("reading")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
"lg:col-span-2",
|
"lg:col-span-2",
|
||||||
selectedModules.includes("reading") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("reading")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsBook className="text-white w-7 h-7" />
|
<BsBook className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Reading</span>
|
<span className="ml-8 font-semibold">Reading</span>
|
||||||
{!selectedModules.includes("reading") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("reading") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("reading") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("reading") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("listening")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
"lg:col-span-2",
|
"lg:col-span-2",
|
||||||
selectedModules.includes("listening") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("listening")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsHeadphones className="text-white w-7 h-7" />
|
<BsHeadphones className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Listening</span>
|
<span className="ml-8 font-semibold">Listening</span>
|
||||||
{!selectedModules.includes("listening") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("listening") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("listening") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("listening") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("writing")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
"lg:col-span-2",
|
"lg:col-span-2",
|
||||||
selectedModules.includes("writing") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("writing")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsPen className="text-white w-7 h-7" />
|
<BsPen className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Writing</span>
|
<span className="ml-8 font-semibold">Writing</span>
|
||||||
{!selectedModules.includes("writing") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("writing") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("writing") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("writing") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("speaking")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
"lg:col-span-3",
|
"lg:col-span-3",
|
||||||
selectedModules.includes("speaking") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("speaking")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsMegaphone className="text-white w-7 h-7" />
|
<BsMegaphone className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Speaking</span>
|
<span className="ml-8 font-semibold">Speaking</span>
|
||||||
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("speaking") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("speaking") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("speaking") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={
|
||||||
(!selectedModules.includes("level") && selectedModules.length === 0) || selectedModules.includes("level")
|
(!selectedModules.includes("level") &&
|
||||||
|
selectedModules.length === 0) ||
|
||||||
|
selectedModules.includes("level")
|
||||||
? () => toggleModule("level")
|
? () => toggleModule("level")
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
"lg:col-span-3",
|
"lg:col-span-3",
|
||||||
selectedModules.includes("level") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("level")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsClipboard className="text-white w-7 h-7" />
|
<BsClipboard className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Level</span>
|
<span className="ml-8 font-semibold">Level</span>
|
||||||
{!selectedModules.includes("level") && selectedModules.length === 0 && (
|
{!selectedModules.includes("level") &&
|
||||||
|
selectedModules.length === 0 && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{!selectedModules.includes("level") && selectedModules.length > 0 && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{!selectedModules.includes("level") &&
|
||||||
{selectedModules.includes("level") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
selectedModules.length > 0 && (
|
||||||
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("level") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
onChange={(e) => setName(e)}
|
||||||
|
defaultValue={name}
|
||||||
|
label="Assignment Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Start Date *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Start Date *
|
||||||
|
</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip z-10",
|
"hover:border-mti-purple tooltip z-10",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
popperClassName="!z-20"
|
popperClassName="!z-20"
|
||||||
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
|
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
|
||||||
@@ -221,12 +366,14 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">End Date *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
End Date *
|
||||||
|
</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip z-10",
|
"hover:border-mti-purple tooltip z-10",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
popperClassName="!z-20"
|
popperClassName="!z-20"
|
||||||
filterTime={(date) => moment(date).isAfter(startDate)}
|
filterTime={(date) => moment(date).isAfter(startDate)}
|
||||||
@@ -240,15 +387,24 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
|
|
||||||
{selectedModules.includes("speaking") && (
|
{selectedModules.includes("speaking") && (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Speaking Instructor's Gender
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{value: instructorGender, label: capitalize(instructorGender)}}
|
value={{
|
||||||
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
|
value: instructorGender,
|
||||||
|
label: capitalize(instructorGender),
|
||||||
|
}}
|
||||||
|
onChange={(value) =>
|
||||||
|
value
|
||||||
|
? setInstructorGender(value.value as InstructorGender)
|
||||||
|
: null
|
||||||
|
}
|
||||||
disabled={!selectedModules.includes("speaking") || !!assignment}
|
disabled={!selectedModules.includes("speaking") || !!assignment}
|
||||||
options={[
|
options={[
|
||||||
{value: "male", label: "Male"},
|
{ value: "male", label: "Male" },
|
||||||
{value: "female", label: "Female"},
|
{ value: "female", label: "Female" },
|
||||||
{value: "varied", label: "Varied"},
|
{ value: "varied", label: "Varied" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,20 +419,29 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
<div className="grid md:grid-cols-2 w-full gap-4">
|
<div className="grid md:grid-cols-2 w-full gap-4">
|
||||||
{selectedModules.map((module) => (
|
{selectedModules.map((module) => (
|
||||||
<div key={module} className="flex flex-col gap-3 w-full">
|
<div key={module} className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">{capitalize(module)} Exam</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
{capitalize(module)} Exam
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: examIDs.find((e) => e.module === module)?.id || null,
|
value:
|
||||||
label: examIDs.find((e) => e.module === module)?.id || "",
|
examIDs.find((e) => e.module === module)?.id || null,
|
||||||
|
label:
|
||||||
|
examIDs.find((e) => e.module === module)?.id || "",
|
||||||
}}
|
}}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value
|
value
|
||||||
? setExamIDs((prev) => [...prev.filter((x) => x.module !== module), {id: value.value!, module}])
|
? setExamIDs((prev) => [
|
||||||
: setExamIDs((prev) => prev.filter((x) => x.module !== module))
|
...prev.filter((x) => x.module !== module),
|
||||||
|
{ id: value.value!, module },
|
||||||
|
])
|
||||||
|
: setExamIDs((prev) =>
|
||||||
|
prev.filter((x) => x.module !== module)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
options={exams
|
options={exams
|
||||||
.filter((x) => !x.isDiagnostic && x.module === module)
|
.filter((x) => !x.isDiagnostic && x.module === module)
|
||||||
.map((x) => ({value: x.id, label: x.id}))}
|
.map((x) => ({ value: x.id, label: x.id }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -286,25 +451,37 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="w-full flex flex-col gap-3">
|
<section className="w-full flex flex-col gap-3">
|
||||||
<span className="font-semibold">Assignees ({assignees.length} selected)</span>
|
<span className="font-semibold">
|
||||||
|
Assignees ({assignees.length} selected)
|
||||||
|
</span>
|
||||||
<div className="flex gap-4 overflow-x-scroll scrollbar-hide">
|
<div className="flex gap-4 overflow-x-scroll scrollbar-hide">
|
||||||
{groups.map((g) => (
|
{groups.map((g) => (
|
||||||
<button
|
<button
|
||||||
key={g.id}
|
key={g.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
|
const groupStudentIds = users
|
||||||
|
.filter((u) => g.participants.includes(u.id))
|
||||||
|
.map((u) => u.id);
|
||||||
if (groupStudentIds.every((u) => assignees.includes(u))) {
|
if (groupStudentIds.every((u) => assignees.includes(u))) {
|
||||||
setAssignees((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
|
setAssignees((prev) =>
|
||||||
|
prev.filter((a) => !groupStudentIds.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setAssignees((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
|
setAssignees((prev) => [
|
||||||
|
...prev.filter((a) => !groupStudentIds.includes(a)),
|
||||||
|
...groupStudentIds,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
|
users
|
||||||
"!bg-mti-purple-light !text-white",
|
.filter((u) => g.participants.includes(u.id))
|
||||||
)}>
|
.every((u) => assignees.includes(u.id)) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{g.name}
|
{g.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -316,9 +493,12 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
assignees.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
|
assignees.includes(user.id)
|
||||||
|
? "border-mti-purple"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
)}
|
)}
|
||||||
key={user.id}>
|
key={user.id}
|
||||||
|
>
|
||||||
<span className="flex flex-col gap-0 justify-center">
|
<span className="flex flex-col gap-0 justify-center">
|
||||||
<span className="font-semibold">{user.name}</span>
|
<span className="font-semibold">{user.name}</span>
|
||||||
<span className="text-sm opacity-80">{user.email}</span>
|
<span className="text-sm opacity-80">{user.email}</span>
|
||||||
@@ -342,27 +522,54 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="flex flex-col gap-4 w-full items-end">
|
<div className="flex flex-col gap-4 w-full items-end">
|
||||||
<Checkbox isChecked={variant === "full"} onChange={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
<Checkbox
|
||||||
|
isChecked={variant === "full"}
|
||||||
|
onChange={() =>
|
||||||
|
setVariant((prev) => (prev === "full" ? "partial" : "full"))
|
||||||
|
}
|
||||||
|
>
|
||||||
Full length exams
|
Full length exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={generateMultiple}
|
||||||
|
onChange={() => setGenerateMultiple((d) => !d)}
|
||||||
|
>
|
||||||
Generate different exams
|
Generate different exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-end">
|
<div className="flex gap-4 w-full justify-end">
|
||||||
<Button className="w-full max-w-[200px]" variant="outline" onClick={cancelCreation} disabled={isLoading} isLoading={isLoading}>
|
<Button
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
variant="outline"
|
||||||
|
onClick={cancelCreation}
|
||||||
|
disabled={isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{assignment && (
|
{assignment && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
color="green"
|
||||||
|
variant="outline"
|
||||||
|
onClick={startAssignment}
|
||||||
|
disabled={isLoading || moment().isAfter(startDate)}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]"
|
||||||
color="red"
|
color="red"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={deleteAssignment}
|
onClick={deleteAssignment}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
@@ -375,7 +582,8 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
}
|
}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]"
|
||||||
onClick={createAssignment}
|
onClick={createAssignment}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
{assignment ? "Update" : "Create"}
|
{assignment ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
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,
|
||||||
@@ -29,34 +29,52 @@ 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 {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
import {
|
||||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
averageLevelCalculator,
|
||||||
import {Module} from "@/interfaces";
|
calculateAverageLevel,
|
||||||
import {groupByExam} from "@/utils/stats";
|
calculateBandScore,
|
||||||
|
} from "@/utils/score";
|
||||||
|
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
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 useAssignments from "@/hooks/useAssignments";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import AssignmentView from "./AssignmentView";
|
import AssignmentView from "./AssignmentView";
|
||||||
import AssignmentCreator from "./AssignmentCreator";
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import AssignmentCard from "./AssignmentCard";
|
import AssignmentCard from "./AssignmentCard";
|
||||||
import {createColumnHelper} from "@tanstack/react-table";
|
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 {
|
||||||
|
futureAssignmentFilter,
|
||||||
|
pastAssignmentFilter,
|
||||||
|
archivedAssignmentFilter,
|
||||||
|
activeAssignmentFilter,
|
||||||
|
} from "@/utils/assignments";
|
||||||
import useUserBalance from "@/hooks/useUserBalance";
|
import useUserBalance from "@/hooks/useUserBalance";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: CorporateUser;
|
user: CorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StudentPerformanceItem = User & {corporateName: string; group: string};
|
type StudentPerformanceItem = User & { corporateName: string; group: string };
|
||||||
const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]}) => {
|
const StudentPerformanceList = ({
|
||||||
|
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>();
|
||||||
@@ -87,35 +105,81 @@ const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanc
|
|||||||
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",
|
||||||
@@ -124,9 +188,15 @@ const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanc
|
|||||||
!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`,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -140,12 +210,12 @@ const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanc
|
|||||||
(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}
|
||||||
/>
|
/>
|
||||||
@@ -153,20 +223,25 @@ const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanc
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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] = useState<CorporateUser>();
|
const [corporateUserToShow, setCorporateUserToShow] =
|
||||||
|
useState<CorporateUser>();
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
|
|
||||||
const {stats} = useStats();
|
const { stats } = useStats();
|
||||||
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 {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
|
const {
|
||||||
const {balance} = useUserBalance();
|
assignments,
|
||||||
|
isLoading: isAssignmentsLoading,
|
||||||
|
reload: reloadAssignments,
|
||||||
|
} = useAssignments({ corporate: user.id });
|
||||||
|
const { balance } = useUserBalance();
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -180,16 +255,26 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
getUserCorporate(user.id).then(setCorporateUserToShow);
|
getUserCorporate(user.id).then(setCorporateUserToShow);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
|
const studentFilter = (user: User) =>
|
||||||
const teacherFilter = (user: User) => user.type === "teacher" && groups.flatMap((g) => g.participants).includes(user.id);
|
user.type === "student" &&
|
||||||
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
|
const teacherFilter = (user: User) =>
|
||||||
|
user.type === "teacher" &&
|
||||||
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
|
|
||||||
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
const getStatsByStudent = (user: User) =>
|
||||||
|
stats.filter((s) => s.user === user.id);
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
onClick={() => setSelectedUser(displayUser)}
|
||||||
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
|
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
||||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
>
|
||||||
|
<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>
|
||||||
@@ -215,7 +300,8 @@ 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>
|
||||||
@@ -244,7 +330,8 @@ 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>
|
||||||
@@ -256,18 +343,22 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GroupsList = () => {
|
const GroupsList = () => {
|
||||||
const filter = (x: Group) => x.admin === user.id || x.participants.includes(user.id);
|
const filter = (x: Group) =>
|
||||||
|
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">
|
||||||
|
Groups ({groups.filter(filter).length})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -276,12 +367,6 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) =>
|
|
||||||
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
|
||||||
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
|
||||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AssignmentView
|
<AssignmentView
|
||||||
@@ -295,7 +380,9 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
/>
|
/>
|
||||||
<AssignmentCreator
|
<AssignmentCreator
|
||||||
assignment={selectedAssignment}
|
assignment={selectedAssignment}
|
||||||
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
groups={groups.filter(
|
||||||
|
(x) => x.admin === user.id || x.participants.includes(user.id)
|
||||||
|
)}
|
||||||
users={users.filter(
|
users={users.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
@@ -304,7 +391,7 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
.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))
|
||||||
)}
|
)}
|
||||||
assigner={user.id}
|
assigner={user.id}
|
||||||
isCreating={isCreatingAssignment}
|
isCreating={isCreatingAssignment}
|
||||||
@@ -317,35 +404,54 @@ 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 className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
<BsArrowRepeat
|
||||||
|
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">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Active Assignments (
|
||||||
|
{assignments.filter(activeAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
<AssignmentCard
|
||||||
|
{...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">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
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>
|
||||||
{assignments.filter(futureFilter).map((a) => (
|
{assignments.filter(futureAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -359,9 +465,11 @@ 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">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -376,9 +484,12 @@ 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">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Archived Assignments (
|
||||||
|
{assignments.filter(archivedAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -398,7 +509,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
|
|
||||||
const StudentPerformancePage = () => {
|
const StudentPerformancePage = () => {
|
||||||
const students = users
|
const students = users
|
||||||
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
|
.filter(
|
||||||
|
(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",
|
||||||
@@ -410,15 +525,19 @@ 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 className={clsx("text-xl", isLoading && "animate-spin")} />
|
<BsArrowRepeat
|
||||||
|
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} />
|
||||||
@@ -436,10 +555,15 @@ 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(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(
|
||||||
|
s.score.correct,
|
||||||
|
s.score.total,
|
||||||
|
s.module,
|
||||||
|
s.focus!
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: {[key in Module]: number} = {
|
const levels: { [key in Module]: number } = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
@@ -455,7 +579,11 @@ 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: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
|
Linked to:{" "}
|
||||||
|
<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">
|
||||||
@@ -476,26 +604,46 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
value={
|
||||||
|
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(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
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"
|
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}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
value={`${balance}/${
|
||||||
|
user.corporateInformation?.companyInformation?.userAmount || 0
|
||||||
|
}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
value={
|
||||||
|
user.subscriptionExpirationDate
|
||||||
|
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
||||||
|
: "Unlimited"
|
||||||
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -508,12 +656,15 @@ 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 ? "Loading..." : assignments.filter((a) => !a.archived).length}
|
{isAssignmentsLoading
|
||||||
|
? "Loading..."
|
||||||
|
: assignments.filter((a) => !a.archived).length}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -547,7 +698,11 @@ 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((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -560,7 +715,8 @@ 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(a))).length,
|
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(a))).length
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -584,7 +740,8 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -594,7 +751,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -604,7 +765,8 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "student"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -614,7 +776,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {IconType} from "react-icons";
|
import { IconType } from "react-icons";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
Icon: IconType;
|
Icon: IconType;
|
||||||
label: string;
|
label: string;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
color: "purple" | "rose" | "red" | "green";
|
color: "purple" | "rose" | "red" | "green";
|
||||||
className?: string;
|
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
isSelected?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function IconCard({Icon, label, value, color, tooltip, className, onClick}: Props) {
|
export default function IconCard({
|
||||||
const colorClasses: {[key in typeof color]: string} = {
|
Icon,
|
||||||
purple: "text-mti-purple-light",
|
label,
|
||||||
red: "text-mti-red-light",
|
value,
|
||||||
rose: "text-mti-rose-light",
|
color,
|
||||||
green: "text-mti-green-light",
|
tooltip,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
isSelected,
|
||||||
|
}: Props) {
|
||||||
|
const colorClasses: { [key in typeof color]: string } = {
|
||||||
|
purple: "mti-purple-light",
|
||||||
|
red: "mti-red-light",
|
||||||
|
rose: "mti-rose-light",
|
||||||
|
green: "mti-green-light",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,13 +35,17 @@ export default function IconCard({Icon, label, value, color, tooltip, className,
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center text-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300",
|
"bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center text-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300",
|
||||||
tooltip && "tooltip tooltip-bottom",
|
tooltip && "tooltip tooltip-bottom",
|
||||||
|
isSelected && `border border-solid border-${colorClasses[color]}`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-tip={tooltip}>
|
data-tip={tooltip}
|
||||||
<Icon className={clsx("text-6xl", colorClasses[color])} />
|
>
|
||||||
|
<Icon className={clsx("text-6xl", `text-${colorClasses[color]}`)} />
|
||||||
<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">{label}</span>
|
<span className="text-lg">{label}</span>
|
||||||
<span className={clsx("font-semibold", colorClasses[color])}>{value}</span>
|
<span className={clsx("font-semibold", `text-${colorClasses[color]}`)}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
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, MasterCorporateUser, Stat, User} from "@/interfaces/user";
|
import {
|
||||||
|
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";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
BsArrowLeft,
|
BsArrowLeft,
|
||||||
BsClipboard2Data,
|
BsClipboard2Data,
|
||||||
@@ -27,61 +33,86 @@ import {
|
|||||||
import UserCard from "@/components/UserCard";
|
import UserCard from "@/components/UserCard";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
|
||||||
import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
import {
|
||||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
averageLevelCalculator,
|
||||||
import {Module} from "@/interfaces";
|
calculateAverageLevel,
|
||||||
import {groupByExam} from "@/utils/stats";
|
calculateBandScore,
|
||||||
|
} from "@/utils/score";
|
||||||
|
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
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 useAssignments from "@/hooks/useAssignments";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import AssignmentView from "./AssignmentView";
|
import AssignmentView from "./AssignmentView";
|
||||||
import AssignmentCreator from "./AssignmentCreator";
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import AssignmentCard from "./AssignmentCard";
|
import AssignmentCard from "./AssignmentCard";
|
||||||
import {createColumn, createColumnHelper} from "@tanstack/react-table";
|
import { createColumn, createColumnHelper } from "@tanstack/react-table";
|
||||||
import List from "@/components/List";
|
import List from "@/components/List";
|
||||||
import {getUserCorporate} from "@/utils/groups";
|
import { getUserCorporate } from "@/utils/groups";
|
||||||
import {getCorporateUser, getUserCompanyName} from "@/resources/user";
|
import { getCorporateUser, getUserCompanyName } from "@/resources/user";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
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 {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import MasterStatistical from "./MasterStatistical";
|
import MasterStatistical from "./MasterStatistical";
|
||||||
|
import {
|
||||||
|
futureAssignmentFilter,
|
||||||
|
pastAssignmentFilter,
|
||||||
|
archivedAssignmentFilter,
|
||||||
|
activeAssignmentFilter,
|
||||||
|
} from "@/utils/assignments";
|
||||||
import useUserBalance from "@/hooks/useUserBalance";
|
import useUserBalance from "@/hooks/useUserBalance";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: MasterCorporateUser;
|
user: MasterCorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeFilter = (a: Assignment) =>
|
type StudentPerformanceItem = User & {
|
||||||
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
corporate?: CorporateUser;
|
||||||
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
group?: Group;
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
};
|
||||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
const StudentPerformanceList = ({
|
||||||
|
items,
|
||||||
type StudentPerformanceItem = User & {corporate?: CorporateUser; group?: Group};
|
stats,
|
||||||
const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => {
|
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<CorporateUser | null | undefined>(null);
|
const [selectedCorporate, setSelectedCorporate] = useState<
|
||||||
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(null);
|
CorporateUser | null | undefined
|
||||||
|
>(null);
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<StudentPerformanceItem>();
|
const columnHelper = createColumnHelper<StudentPerformanceItem>();
|
||||||
|
|
||||||
@@ -104,7 +135,10 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("corporate", {
|
columnHelper.accessor("corporate", {
|
||||||
header: "Corporate",
|
header: "Corporate",
|
||||||
cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"),
|
cell: (info) =>
|
||||||
|
!!info.getValue()
|
||||||
|
? getUserCompanyName(info.getValue() as User, users, groups)
|
||||||
|
: "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("levels.reading", {
|
columnHelper.accessor("levels.reading", {
|
||||||
header: "Reading",
|
header: "Reading",
|
||||||
@@ -112,15 +146,30 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!isShowingAmount
|
!isShowingAmount
|
||||||
? calculateBandScore(
|
? calculateBandScore(
|
||||||
stats
|
stats
|
||||||
.filter((x) => x.module === "reading" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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((x) => x.module === "reading" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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",
|
||||||
@@ -128,15 +177,31 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!isShowingAmount
|
!isShowingAmount
|
||||||
? calculateBandScore(
|
? calculateBandScore(
|
||||||
stats
|
stats
|
||||||
.filter((x) => x.module === "listening" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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((x) => x.module === "listening" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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",
|
||||||
@@ -144,15 +209,30 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!isShowingAmount
|
!isShowingAmount
|
||||||
? calculateBandScore(
|
? calculateBandScore(
|
||||||
stats
|
stats
|
||||||
.filter((x) => x.module === "writing" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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((x) => x.module === "writing" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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",
|
||||||
@@ -160,15 +240,30 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!isShowingAmount
|
!isShowingAmount
|
||||||
? calculateBandScore(
|
? calculateBandScore(
|
||||||
stats
|
stats
|
||||||
.filter((x) => x.module === "speaking" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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((x) => x.module === "speaking" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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",
|
||||||
@@ -176,15 +271,28 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!isShowingAmount
|
!isShowingAmount
|
||||||
? calculateBandScore(
|
? calculateBandScore(
|
||||||
stats
|
stats
|
||||||
.filter((x) => x.module === "level" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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((x) => x.module === "level" && x.user === info.row.original.id)
|
.filter(
|
||||||
|
(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",
|
||||||
@@ -193,16 +301,24 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!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) => item.corporate?.id === selectedCorporate?.id;
|
const filterByCorporate = (item: StudentPerformanceItem) =>
|
||||||
const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id;
|
item.corporate?.id === selectedCorporate?.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);
|
||||||
@@ -229,7 +345,10 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
<Select
|
<Select
|
||||||
options={availableCorporates.map((x) => ({
|
options={availableCorporates.map((x) => ({
|
||||||
value: x?.id || "N/A",
|
value: x?.id || "N/A",
|
||||||
label: x?.corporateInformation?.companyInformation?.name || x?.name || "N/A",
|
label:
|
||||||
|
x?.corporateInformation?.companyInformation?.name ||
|
||||||
|
x?.name ||
|
||||||
|
"N/A",
|
||||||
}))}
|
}))}
|
||||||
isClearable
|
isClearable
|
||||||
value={
|
value={
|
||||||
@@ -238,7 +357,8 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
: {
|
: {
|
||||||
value: selectedCorporate?.id || "N/A",
|
value: selectedCorporate?.id || "N/A",
|
||||||
label:
|
label:
|
||||||
selectedCorporate?.corporateInformation?.companyInformation?.name ||
|
selectedCorporate?.corporateInformation
|
||||||
|
?.companyInformation?.name ||
|
||||||
selectedCorporate?.name ||
|
selectedCorporate?.name ||
|
||||||
"N/A",
|
"N/A",
|
||||||
}
|
}
|
||||||
@@ -248,7 +368,11 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
!value
|
!value
|
||||||
? setSelectedCorporate(null)
|
? setSelectedCorporate(null)
|
||||||
: setSelectedCorporate(
|
: setSelectedCorporate(
|
||||||
value.value === "N/A" ? undefined : availableCorporates.find((x) => x?.id === value.value),
|
value.value === "N/A"
|
||||||
|
? undefined
|
||||||
|
: availableCorporates.find(
|
||||||
|
(x) => x?.id === value.value
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -270,7 +394,11 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
!value
|
!value
|
||||||
? setSelectedGroup(null)
|
? setSelectedGroup(null)
|
||||||
: setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value))
|
: setSelectedGroup(
|
||||||
|
value.value === "N/A"
|
||||||
|
? undefined
|
||||||
|
: availableGroups.find((x) => x?.id === value.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,13 +411,13 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
(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}
|
||||||
/>
|
/>
|
||||||
@@ -297,24 +425,37 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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 [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]);
|
const [corporateAssignments, setCorporateAssignments] = useState<
|
||||||
|
(Assignment & { corporate?: CorporateUser })[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
const {stats} = useStats();
|
const { stats } = useStats();
|
||||||
const {users, reload} = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const {groups} = useGroups({admin: user.id, userType: user.type});
|
const { codes } = useCodes(user.id);
|
||||||
const {balance} = useUserBalance();
|
const { groups } = useGroups({ admin: user.id, userType: user.type });
|
||||||
|
const { balance } = useUserBalance();
|
||||||
|
|
||||||
const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))];
|
const masterCorporateUserGroups = useMemo(() => [
|
||||||
|
...new Set(
|
||||||
|
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
|
||||||
|
),
|
||||||
|
], [groups, user.id]);
|
||||||
|
|
||||||
const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))];
|
const corporateUserGroups = [
|
||||||
|
...new Set(groups.flatMap((g) => g.participants)),
|
||||||
|
];
|
||||||
|
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id});
|
const {
|
||||||
|
assignments,
|
||||||
|
isLoading: isAssignmentsLoading,
|
||||||
|
reload: reloadAssignments,
|
||||||
|
} = useAssignments({ corporate: user.id });
|
||||||
|
|
||||||
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -325,24 +466,36 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCorporateAssignments(
|
setCorporateAssignments(
|
||||||
assignments.filter(activeFilter).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(users.find((x) => x.id === a.assigner)!, users, groups)
|
? getCorporateUser(
|
||||||
|
users.find((x) => x.id === a.assigner)!,
|
||||||
|
users,
|
||||||
|
groups
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
}, [assignments, groups, users]);
|
}, [assignments, groups, users]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
|
const studentFilter = (user: User) =>
|
||||||
const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
|
user.type === "student" && corporateUserGroups.includes(user.id);
|
||||||
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
const teacherFilter = (user: User) =>
|
||||||
|
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>
|
||||||
@@ -352,7 +505,10 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
|
|
||||||
const StudentsList = () => {
|
const StudentsList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) =>
|
||||||
x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
|
x.type === "student" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? corporateUserGroups.includes(x.id) || false
|
||||||
|
: corporateUserGroups.includes(x.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
@@ -362,7 +518,8 @@ 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>
|
||||||
@@ -375,7 +532,10 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
|
|
||||||
const TeachersList = () => {
|
const TeachersList = () => {
|
||||||
const filter = (x: User) =>
|
const filter = (x: User) =>
|
||||||
x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
|
x.type === "teacher" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? corporateUserGroups.includes(x.id) || false
|
||||||
|
: corporateUserGroups.includes(x.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserList
|
<UserList
|
||||||
@@ -385,7 +545,8 @@ 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>
|
||||||
@@ -397,7 +558,10 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const corporateUserFilter = (x: User) =>
|
const corporateUserFilter = (x: User) =>
|
||||||
x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
|
x.type === "corporate" &&
|
||||||
|
(!!selectedUser
|
||||||
|
? masterCorporateUserGroups.includes(x.id) || false
|
||||||
|
: masterCorporateUserGroups.includes(x.id));
|
||||||
|
|
||||||
const CorporateList = () => {
|
const CorporateList = () => {
|
||||||
return (
|
return (
|
||||||
@@ -408,7 +572,8 @@ 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>
|
||||||
@@ -425,7 +590,8 @@ 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>
|
||||||
@@ -437,22 +603,13 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const AssignmentsPage = () => {
|
|
||||||
// const activeFilter = (a: Assignment) =>
|
|
||||||
// moment(a.endDate).isAfter(moment()) &&
|
|
||||||
// moment(a.startDate).isBefore(moment()) &&
|
|
||||||
// a.assignees.length > a.results.length;
|
|
||||||
// const pastFilter = (a: Assignment) =>
|
|
||||||
// (moment(a.endDate).isBefore(moment()) ||
|
|
||||||
// a.assignees.length === a.results.length) &&
|
|
||||||
// !a.archived;
|
|
||||||
// const archivedFilter = (a: Assignment) => a.archived;
|
|
||||||
// const futureFilter = (a: Assignment) =>
|
|
||||||
// moment(a.startDate).isAfter(moment());
|
|
||||||
|
|
||||||
const StudentPerformancePage = () => {
|
const StudentPerformancePage = () => {
|
||||||
const students = users
|
const students = users
|
||||||
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id))
|
.filter(
|
||||||
|
(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)),
|
||||||
@@ -464,18 +621,30 @@ 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 className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
<BsArrowRepeat
|
||||||
|
className={clsx(
|
||||||
|
"text-xl",
|
||||||
|
isAssignmentsLoading && "animate-spin"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StudentPerformanceList items={students} stats={stats} users={users} groups={groups} />
|
<StudentPerformanceList
|
||||||
|
items={students}
|
||||||
|
stats={stats}
|
||||||
|
users={users}
|
||||||
|
groups={groups}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -494,7 +663,9 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
/>
|
/>
|
||||||
<AssignmentCreator
|
<AssignmentCreator
|
||||||
assignment={selectedAssignment}
|
assignment={selectedAssignment}
|
||||||
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
groups={groups.filter(
|
||||||
|
(x) => x.admin === user.id || x.participants.includes(user.id)
|
||||||
|
)}
|
||||||
users={users.filter(
|
users={users.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
@@ -503,7 +674,7 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
.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))
|
||||||
)}
|
)}
|
||||||
assigner={user.id}
|
assigner={user.id}
|
||||||
isCreating={isCreatingAssignment}
|
isCreating={isCreatingAssignment}
|
||||||
@@ -516,53 +687,92 @@ 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 className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
<BsArrowRepeat
|
||||||
|
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> {assignments.filter(activeFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/
|
<b>Total:</b>{" "}
|
||||||
{assignments.filter(activeFilter).reduce((acc, curr) => curr.exams.length + acc, 0)}
|
{assignments
|
||||||
|
.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(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => (
|
{Object.keys(
|
||||||
|
groupBy(corporateAssignments, (x) => x.corporate?.id)
|
||||||
|
).map((x) => (
|
||||||
<div key={x}>
|
<div key={x}>
|
||||||
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span>
|
<span className="font-semibold">
|
||||||
|
{getUserCompanyName(
|
||||||
|
users.find((u) => u.id === x)!,
|
||||||
|
users,
|
||||||
|
groups
|
||||||
|
)}
|
||||||
|
:{" "}
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/
|
{groupBy(corporateAssignments, (x) => x.corporate?.id)[
|
||||||
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)}
|
x
|
||||||
|
].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">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Active Assignments (
|
||||||
|
{assignments.filter(activeAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
<AssignmentCard
|
||||||
|
{...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">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
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>
|
||||||
{assignments.filter(futureFilter).map((a) => (
|
{assignments.filter(futureAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -576,9 +786,11 @@ 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">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -593,9 +805,12 @@ 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">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Archived Assignments (
|
||||||
|
{assignments.filter(archivedAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -613,24 +828,32 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const masterCorporateUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
masterCorporateUserGroups.reduce((accm: CorporateUser[], id) => {
|
||||||
|
const user = users.find((u) => u.id === id) as CorporateUser;
|
||||||
|
if (user) return [...accm, user];
|
||||||
|
return accm;
|
||||||
|
}, []),
|
||||||
|
[masterCorporateUserGroups, users]
|
||||||
|
);
|
||||||
|
|
||||||
const MasterStatisticalPage = () => {
|
const MasterStatisticalPage = () => {
|
||||||
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">Master Statistical</h2>
|
<h2 className="text-2xl font-semibold">Master Statistical</h2>
|
||||||
</div>
|
</div>
|
||||||
<MasterStatistical
|
<MasterStatistical
|
||||||
users={masterCorporateUserGroups.reduce((accm: CorporateUser[], id) => {
|
users={users}
|
||||||
const user = users.find((u) => u.id === id) as CorporateUser;
|
corporateUsers={masterCorporateUsers}
|
||||||
if (user) return [...accm, user];
|
|
||||||
return accm;
|
|
||||||
}, [])}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -656,7 +879,11 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
value={
|
||||||
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
).length
|
||||||
|
}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -664,21 +891,35 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
label="Average Level"
|
label="Average Level"
|
||||||
value={averageLevelCalculator(
|
value={averageLevelCalculator(
|
||||||
users,
|
users,
|
||||||
stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)),
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
)
|
||||||
).toFixed(1)}
|
).toFixed(1)}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} 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}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
value={`${balance}/${
|
||||||
|
user.corporateInformation?.companyInformation?.userAmount || 0
|
||||||
|
}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
value={
|
||||||
|
user.subscriptionExpirationDate
|
||||||
|
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
||||||
|
: "Unlimited"
|
||||||
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -695,22 +936,25 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => setPage("studentsPerformance")}
|
onClick={() => setPage("studentsPerformance")}
|
||||||
/>
|
/>
|
||||||
{/* <IconCard
|
<IconCard
|
||||||
Icon={BsDatabase}
|
Icon={BsDatabase}
|
||||||
label="Master Statistical"
|
label="Master Statistical"
|
||||||
// value={masterCorporateUserGroups.length}
|
// value={masterCorporateUserGroups.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => setPage("statistical")}
|
onClick={() => setPage("statistical")}
|
||||||
/> */}
|
/>
|
||||||
<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 ? "Loading..." : assignments.filter((a) => !a.archived).length}
|
{isAssignmentsLoading
|
||||||
|
? "Loading..."
|
||||||
|
: assignments.filter((a) => !a.archived).length}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -744,7 +988,11 @@ 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((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -757,7 +1005,8 @@ 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(a))).length,
|
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(a))).length
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -777,7 +1026,8 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
<UserCard
|
<UserCard
|
||||||
maxUserAmount={
|
maxUserAmount={
|
||||||
user.type === "mastercorporate"
|
user.type === "mastercorporate"
|
||||||
? (user.corporateInformation?.companyInformation?.userAmount || 0) - balance
|
? (user.corporateInformation?.companyInformation
|
||||||
|
?.userAmount || 0) - balance
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
loggedInUser={user}
|
loggedInUser={user}
|
||||||
@@ -786,7 +1036,8 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "teacher"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-students",
|
id: "view-students",
|
||||||
@@ -796,7 +1047,11 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
@@ -806,7 +1061,8 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={
|
onViewTeachers={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "student"
|
selectedUser.type === "corporate" ||
|
||||||
|
selectedUser.type === "student"
|
||||||
? () => {
|
? () => {
|
||||||
appendUserFilters({
|
appendUserFilters({
|
||||||
id: "view-teachers",
|
id: "view-teachers",
|
||||||
@@ -816,7 +1072,11 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
id: "belongs-to-admin",
|
id: "belongs-to-admin",
|
||||||
filter: (x: User) =>
|
filter: (x: User) =>
|
||||||
groups
|
groups
|
||||||
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
.filter(
|
||||||
|
(g) =>
|
||||||
|
g.admin === selectedUser.id ||
|
||||||
|
g.participants.includes(selectedUser.id)
|
||||||
|
)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id),
|
.includes(x.id),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,341 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {CorporateUser} from "@/interfaces/user";
|
import { CorporateUser, User } from "@/interfaces/user";
|
||||||
import {BsBank, BsPersonFill} from "react-icons/bs";
|
import { BsBank, BsPersonFill } from "react-icons/bs";
|
||||||
import IconCard from "./IconCard";
|
import IconCard from "./IconCard";
|
||||||
import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates";
|
import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates";
|
||||||
|
import ReactDatePicker from "react-datepicker";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Assignment, AssignmentWithCorporateId } from "@/interfaces/results";
|
||||||
|
import {
|
||||||
|
CellContext,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
HeaderGroup,
|
||||||
|
Table,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
users: CorporateUser[];
|
corporateUsers: User[];
|
||||||
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
user: string;
|
||||||
|
correct: number;
|
||||||
|
corporate: string;
|
||||||
|
submitted: boolean;
|
||||||
|
date: moment.Moment;
|
||||||
|
assignment: string;
|
||||||
|
corporateId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserCount {
|
||||||
|
userCount: number;
|
||||||
|
maxUserCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
const MasterStatistical = (props: Props) => {
|
const MasterStatistical = (props: Props) => {
|
||||||
const {users} = props;
|
const { users, corporateUsers } = props;
|
||||||
|
|
||||||
const usersList = React.useMemo(() => users.map((x) => x.id), [users]);
|
const corporateRelevantUsers = React.useMemo(
|
||||||
|
() => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[],
|
||||||
|
[corporateUsers]
|
||||||
|
);
|
||||||
|
|
||||||
const {assignments} = useAssignmentsCorporates({corporates: usersList});
|
const corporates = React.useMemo(
|
||||||
|
() => corporateRelevantUsers.map((x) => x.id),
|
||||||
|
[corporateRelevantUsers]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedCorporates, setSelectedCorporates] =
|
||||||
|
React.useState<string[]>(corporates);
|
||||||
|
const [startDate, setStartDate] = React.useState<Date | null>(
|
||||||
|
moment("01/01/2023").toDate()
|
||||||
|
);
|
||||||
|
const [endDate, setEndDate] = React.useState<Date | null>(
|
||||||
|
moment().endOf("year").toDate()
|
||||||
|
);
|
||||||
|
|
||||||
|
const { assignments } = useAssignmentsCorporates({
|
||||||
|
// corporates: [...corporates, "tYU0HTiJdjMsS8SB7XJsUdMMP892"],
|
||||||
|
corporates: selectedCorporates,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableResults = React.useMemo(
|
||||||
|
() =>
|
||||||
|
assignments.reduce((accmA: TableData[], a: AssignmentWithCorporateId) => {
|
||||||
|
const userResults = a.assignees.map((assignee) => {
|
||||||
|
const userStats =
|
||||||
|
a.results.find((r) => r.user === assignee)?.stats || [];
|
||||||
|
const userName = users.find((u) => u.id === assignee)?.name || "";
|
||||||
|
const corporate = users.find((u) => u.id === a.assigner)?.name || "";
|
||||||
|
const commonData = {
|
||||||
|
user: userName,
|
||||||
|
corporateId: a.corporateId,
|
||||||
|
corporate,
|
||||||
|
assignment: a.name,
|
||||||
|
};
|
||||||
|
if (userStats.length === 0) {
|
||||||
|
return {
|
||||||
|
...commonData,
|
||||||
|
correct: 0,
|
||||||
|
submitted: false,
|
||||||
|
// date: moment(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commonData,
|
||||||
|
correct: userStats.reduce((n, e) => n + e.score.correct, 0),
|
||||||
|
submitted: true,
|
||||||
|
date: moment.max(userStats.map((e) => moment(e.date))),
|
||||||
|
};
|
||||||
|
}) as TableData[];
|
||||||
|
|
||||||
|
return [...accmA, ...userResults];
|
||||||
|
}, []),
|
||||||
|
[assignments, users]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCorporateScores = (corporateId: string): UserCount => {
|
||||||
|
const corporateAssignmentsUsers = assignments
|
||||||
|
.filter((a) => a.corporateId === corporateId)
|
||||||
|
.reduce((acc, a) => acc + a.assignees.length, 0);
|
||||||
|
|
||||||
|
const corporateResults = tableResults.filter(
|
||||||
|
(r) => r.corporateId === corporateId
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxUserCount: corporateAssignmentsUsers,
|
||||||
|
userCount: corporateResults,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const corporateScores = corporates.reduce(
|
||||||
|
(accm, id) => ({
|
||||||
|
...accm,
|
||||||
|
[id]: getCorporateScores(id),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
) as Record<string, UserCount>;
|
||||||
|
|
||||||
|
const consolidateScore = Object.values(corporateScores).reduce(
|
||||||
|
(acc: UserCount, { userCount, maxUserCount }: UserCount) => ({
|
||||||
|
userCount: acc.userCount + userCount,
|
||||||
|
maxUserCount: acc.maxUserCount + maxUserCount,
|
||||||
|
}),
|
||||||
|
{ userCount: 0, maxUserCount: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const getConsolidateScoreStr = (data: UserCount) =>
|
||||||
|
`${data.userCount}/${data.maxUserCount}`;
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<TableData>();
|
||||||
|
|
||||||
|
const defaultColumns = [
|
||||||
|
columnHelper.accessor("user", {
|
||||||
|
header: "User",
|
||||||
|
id: "user",
|
||||||
|
cell: (info) => {
|
||||||
|
return <span>{info.getValue()}</span>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("corporate", {
|
||||||
|
header: "Corporate",
|
||||||
|
id: "corporate",
|
||||||
|
cell: (info) => {
|
||||||
|
return <span>{info.getValue()}</span>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("assignment", {
|
||||||
|
header: "Assignment",
|
||||||
|
id: "assignment",
|
||||||
|
cell: (info) => {
|
||||||
|
return <span>{info.getValue()}</span>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("submitted", {
|
||||||
|
header: "Submitted",
|
||||||
|
id: "submitted",
|
||||||
|
cell: (info) => {
|
||||||
return (
|
return (
|
||||||
|
<Checkbox isChecked={info.getValue()} disabled onChange={() => {}}>
|
||||||
|
<span></span>
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("correct", {
|
||||||
|
header: "Correct",
|
||||||
|
id: "correct",
|
||||||
|
cell: (info) => {
|
||||||
|
return <span>{info.getValue()}</span>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("date", {
|
||||||
|
header: "Date",
|
||||||
|
id: "date",
|
||||||
|
cell: (info) => {
|
||||||
|
const date = info.getValue();
|
||||||
|
if (date) {
|
||||||
|
return <span>{date.format("DD/MM/YYYY")}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{""}</span>;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: tableResults,
|
||||||
|
columns: defaultColumns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const areAllSelected = selectedCorporates.length === corporates.length;
|
||||||
|
|
||||||
|
const getStudentsConsolidateScore = () => {
|
||||||
|
if (tableResults.length === 0) {
|
||||||
|
return { highest: null, lowest: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the student with the highest and lowest score
|
||||||
|
return tableResults.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
if (curr.correct > acc.highest.correct) {
|
||||||
|
acc.highest = curr;
|
||||||
|
}
|
||||||
|
if (curr.correct < acc.lowest.correct) {
|
||||||
|
acc.lowest = curr;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ highest: tableResults[0], lowest: tableResults[0] }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const consolidateResults = getStudentsConsolidateScore();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<div className="flex flex-wrap gap-2 items-center text-center">
|
<div className="flex flex-wrap gap-2 items-center text-center">
|
||||||
<IconCard Icon={BsBank} label="Consolidate" value={0} color="purple" onClick={() => console.log("clicked")} />
|
<IconCard
|
||||||
{users.map((group) => (
|
Icon={BsBank}
|
||||||
|
label="Consolidate"
|
||||||
|
value={getConsolidateScoreStr(consolidateScore)}
|
||||||
|
color="purple"
|
||||||
|
onClick={() => {
|
||||||
|
if (areAllSelected) {
|
||||||
|
setSelectedCorporates([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedCorporates(corporates);
|
||||||
|
}}
|
||||||
|
isSelected={areAllSelected}
|
||||||
|
/>
|
||||||
|
{corporateRelevantUsers.map((group) => {
|
||||||
|
const isSelected = selectedCorporates.includes(group.id);
|
||||||
|
return (
|
||||||
<IconCard
|
<IconCard
|
||||||
key={group.id}
|
key={group.id}
|
||||||
Icon={BsBank}
|
Icon={BsBank}
|
||||||
label={group.corporateInformation?.companyInformation?.name}
|
label={group.corporateInformation?.companyInformation?.name}
|
||||||
value={0}
|
value={getConsolidateScoreStr(corporateScores[group.id])}
|
||||||
color="purple"
|
color="purple"
|
||||||
onClick={() => console.log("clicked", group)}
|
onClick={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
setSelectedCorporates(
|
||||||
|
selectedCorporates.filter((x) => x !== group.id)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedCorporates([...selectedCorporates, group.id]);
|
||||||
|
}}
|
||||||
|
isSelected={isSelected}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
<IconCard onClick={() => console.log("clicked")} Icon={BsPersonFill} label="Consolidate Highest Student" color="purple" />
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 w-full">
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Date</label>
|
||||||
|
<ReactDatePicker
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
className="px-4 py-6 w-52 text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
|
selected={startDate}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
selectsRange
|
||||||
|
showMonthDropdown
|
||||||
|
onChange={([initialDate, finalDate]: [Date, Date]) => {
|
||||||
|
setStartDate(initialDate ?? moment("01/01/2023").toDate());
|
||||||
|
if (finalDate) {
|
||||||
|
// basicly selecting a final day works as if I'm selecting the first
|
||||||
|
// minute of that day. this way it covers the whole day
|
||||||
|
setEndDate(moment(finalDate).endOf("day").toDate());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEndDate(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full">
|
||||||
|
<thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<th className="p-4 text-left" key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody className="px-2">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<tr
|
||||||
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||||
|
key={row.id}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 items-center text-center">
|
||||||
|
{consolidateResults.highest && (
|
||||||
|
<IconCard
|
||||||
|
onClick={() => {}}
|
||||||
|
Icon={BsPersonFill}
|
||||||
|
label={`Highest result: ${consolidateResults.highest.user}`}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{consolidateResults.lowest && (
|
||||||
|
<IconCard
|
||||||
|
onClick={() => {}}
|
||||||
|
Icon={BsPersonFill}
|
||||||
|
label={`Lowest result: ${consolidateResults.lowest.user}`}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -174,14 +174,14 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
||||||
data-tip="Your screen size is too small to perform an assignment">
|
data-tip="Your screen size is too small to perform an assignment">
|
||||||
<Button
|
<Button
|
||||||
disabled={moment(assignment.startDate).isAfter(moment())}
|
disabled={!assignment.start}
|
||||||
className="h-full w-full !rounded-xl"
|
className="h-full w-full !rounded-xl"
|
||||||
variant="outline">
|
variant="outline">
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
disabled={moment(assignment.startDate).isAfter(moment())}
|
disabled={!assignment.start}
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
onClick={() => startAssignment(assignment)}
|
onClick={() => startAssignment(assignment)}
|
||||||
variant="outline">
|
variant="outline">
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useStats from "@/hooks/useStats";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {CorporateUser, Group, Stat, User} from "@/interfaces/user";
|
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
import {dateSorter} from "@/utils";
|
import { dateSorter } from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
BsArrowLeft,
|
BsArrowLeft,
|
||||||
BsArrowRepeat,
|
BsArrowRepeat,
|
||||||
@@ -31,41 +31,52 @@ 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 {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||||
import {MODULE_ARRAY} from "@/utils/moduleUtils";
|
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import {groupByExam} from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import IconCard from "./IconCard";
|
import IconCard from "./IconCard";
|
||||||
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
import GroupList from "@/pages/(admin)/Lists/GroupList";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import AssignmentCard from "./AssignmentCard";
|
import AssignmentCard from "./AssignmentCard";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import AssignmentCreator from "./AssignmentCreator";
|
import AssignmentCreator from "./AssignmentCreator";
|
||||||
import AssignmentView from "./AssignmentView";
|
import AssignmentView from "./AssignmentView";
|
||||||
import {getUserCorporate} from "@/utils/groups";
|
import { getUserCorporate } from "@/utils/groups";
|
||||||
import {checkAccess} from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
import {
|
||||||
|
futureAssignmentFilter,
|
||||||
|
pastAssignmentFilter,
|
||||||
|
archivedAssignmentFilter,
|
||||||
|
activeAssignmentFilter
|
||||||
|
} from '@/utils/assignments';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeacherDashboard({user}: Props) {
|
export default function TeacherDashboard({ user }: Props) {
|
||||||
const [page, setPage] = useState("");
|
const [page, setPage] = useState("");
|
||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
const [corporateUserToShow, setCorporateUserToShow] = useState<CorporateUser>();
|
const [corporateUserToShow, setCorporateUserToShow] =
|
||||||
|
useState<CorporateUser>();
|
||||||
|
|
||||||
const {stats} = useStats();
|
const { stats } = useStats();
|
||||||
const {users, reload} = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const {groups} = useGroups({adminAdmins: user.id});
|
const { groups } = useGroups({ adminAdmins: user.id });
|
||||||
const {permissions} = usePermissions(user.id);
|
const { permissions } = usePermissions(user.id);
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id});
|
const {
|
||||||
|
assignments,
|
||||||
|
isLoading: isAssignmentsLoading,
|
||||||
|
reload: reloadAssignments,
|
||||||
|
} = useAssignments({ assigner: user.id });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowModal(!!selectedUser && page === "");
|
setShowModal(!!selectedUser && page === "");
|
||||||
@@ -75,15 +86,23 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
getUserCorporate(user.id).then(setCorporateUserToShow);
|
getUserCorporate(user.id).then(setCorporateUserToShow);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
|
const studentFilter = (user: User) =>
|
||||||
|
user.type === "student" &&
|
||||||
|
groups.flatMap((g) => g.participants).includes(user.id);
|
||||||
|
|
||||||
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
const getStatsByStudent = (user: User) =>
|
||||||
|
stats.filter((s) => s.user === user.id);
|
||||||
|
|
||||||
const UserDisplay = (displayUser: User) => (
|
const UserDisplay = (displayUser: User) => (
|
||||||
<div
|
<div
|
||||||
onClick={() => setSelectedUser(displayUser)}
|
onClick={() => setSelectedUser(displayUser)}
|
||||||
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300">
|
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
|
||||||
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" />
|
>
|
||||||
|
<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>
|
||||||
@@ -109,7 +128,8 @@ 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>
|
||||||
@@ -128,11 +148,14 @@ 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">Groups ({groups.filter(filter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Groups ({groups.filter(filter).length})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupList user={user} />
|
<GroupList user={user} />
|
||||||
@@ -150,10 +173,15 @@ 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(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(
|
||||||
|
s.score.correct,
|
||||||
|
s.score.total,
|
||||||
|
s.module,
|
||||||
|
s.focus!
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: {[key in Module]: number} = {
|
const levels: { [key in Module]: number } = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
@@ -166,12 +194,6 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AssignmentsPage = () => {
|
const AssignmentsPage = () => {
|
||||||
const activeFilter = (a: Assignment) =>
|
|
||||||
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length;
|
|
||||||
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived;
|
|
||||||
const archivedFilter = (a: Assignment) => a.archived;
|
|
||||||
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AssignmentView
|
<AssignmentView
|
||||||
@@ -185,7 +207,9 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
/>
|
/>
|
||||||
<AssignmentCreator
|
<AssignmentCreator
|
||||||
assignment={selectedAssignment}
|
assignment={selectedAssignment}
|
||||||
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))}
|
groups={groups.filter(
|
||||||
|
(x) => x.admin === user.id || x.participants.includes(user.id)
|
||||||
|
)}
|
||||||
users={users.filter(
|
users={users.filter(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === "student" &&
|
x.type === "student" &&
|
||||||
@@ -194,7 +218,7 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
.filter((g) => g.admin === selectedUser.id)
|
.filter((g) => g.admin === selectedUser.id)
|
||||||
.flatMap((g) => g.participants)
|
.flatMap((g) => g.participants)
|
||||||
.includes(x.id)
|
.includes(x.id)
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id)),
|
: groups.flatMap((g) => g.participants).includes(x.id))
|
||||||
)}
|
)}
|
||||||
assigner={user.id}
|
assigner={user.id}
|
||||||
isCreating={isCreatingAssignment}
|
isCreating={isCreatingAssignment}
|
||||||
@@ -207,35 +231,52 @@ 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 className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} />
|
<BsArrowRepeat
|
||||||
|
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">Active Assignments ({assignments.filter(activeFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Active Assignments ({assignments.filter(activeAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(activeFilter).map((a) => (
|
{assignments.filter(activeAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} />
|
<AssignmentCard
|
||||||
|
{...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">Planned Assignments ({assignments.filter(futureFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
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>
|
||||||
{assignments.filter(futureFilter).map((a) => (
|
{assignments.filter(futureAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -249,9 +290,11 @@ 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">Past Assignments ({assignments.filter(pastFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(pastFilter).map((a) => (
|
{assignments.filter(pastAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -266,9 +309,11 @@ 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">Archived Assignments ({assignments.filter(archivedFilter).length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{assignments.filter(archivedFilter).map((a) => (
|
{assignments.filter(archivedAssignmentFilter).map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -290,14 +335,19 @@ 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: <b>{corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}</b>
|
Linked to:{" "}
|
||||||
|
<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}
|
||||||
@@ -308,16 +358,29 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClipboard2Data}
|
Icon={BsClipboard2Data}
|
||||||
label="Exams Performed"
|
label="Exams Performed"
|
||||||
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length}
|
value={
|
||||||
|
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(stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user))).toFixed(1)}
|
value={averageLevelCalculator(
|
||||||
|
stats.filter((s) =>
|
||||||
|
groups.flatMap((g) => g.participants).includes(s.user)
|
||||||
|
)
|
||||||
|
).toFixed(1)}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
{checkAccess(user, ["teacher", "developer"], permissions, "viewGroup") && (
|
{checkAccess(
|
||||||
|
user,
|
||||||
|
["teacher", "developer"],
|
||||||
|
permissions,
|
||||||
|
"viewGroup"
|
||||||
|
) && (
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsPeople}
|
Icon={BsPeople}
|
||||||
label="Groups"
|
label="Groups"
|
||||||
@@ -328,11 +391,14 @@ 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">{assignments.filter((a) => !a.archived).length}</span>
|
<span className="font-semibold text-mti-purple-light">
|
||||||
|
{assignments.filter((a) => !a.archived).length}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -354,7 +420,11 @@ 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((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
))}
|
))}
|
||||||
@@ -367,7 +437,8 @@ 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(a))).length,
|
Object.keys(groupByExam(getStatsByStudent(b))).length -
|
||||||
|
Object.keys(groupByExam(getStatsByStudent(a))).length
|
||||||
)
|
)
|
||||||
.map((x) => (
|
.map((x) => (
|
||||||
<UserDisplay key={x.id} {...x} />
|
<UserDisplay key={x.id} {...x} />
|
||||||
@@ -391,9 +462,16 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => setPage("students") : undefined
|
selectedUser.type === "corporate" ||
|
||||||
|
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>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Assignment } from "@/interfaces/results";
|
import { AssignmentWithCorporateId } from "@/interfaces/results";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function useAssignmentsCorporates({
|
export default function useAssignmentsCorporates({
|
||||||
corporates,
|
corporates,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
}: {
|
}: {
|
||||||
corporates: string[];
|
corporates: string[];
|
||||||
|
startDate: Date | null;
|
||||||
|
endDate: Date | null;
|
||||||
}) {
|
}) {
|
||||||
const [assignments, setAssignments] = useState<Assignment[]>([]);
|
const [assignments, setAssignments] = useState<AssignmentWithCorporateId[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
@@ -18,9 +22,15 @@ export default function useAssignmentsCorporates({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
const urlSearchParams = new URLSearchParams({
|
||||||
|
ids: corporates.join(","),
|
||||||
|
...(startDate ? { startDate: startDate.toISOString() } : {}),
|
||||||
|
...(endDate ? { endDate: endDate.toISOString() } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.get<Assignment[]>(
|
.get<AssignmentWithCorporateId[]>(
|
||||||
`/api/assignments/corporate?ids=${corporates.join(",")}`
|
`/api/assignments/corporate?${urlSearchParams.toString()}`
|
||||||
)
|
)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
setAssignments(response.data);
|
setAssignments(response.data);
|
||||||
@@ -28,7 +38,7 @@ export default function useAssignmentsCorporates({
|
|||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(getData, [corporates]);
|
useEffect(getData, [corporates, startDate, endDate]);
|
||||||
|
|
||||||
return { assignments, isLoading, isError, reload: getData };
|
return { assignments, isLoading, isError, reload: getData };
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/hooks/useAssignmentRelease.tsx
Normal file
42
src/hooks/useAssignmentRelease.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {BsDoorOpen} from "react-icons/bs";
|
||||||
|
|
||||||
|
export const useAssignmentRelease = (assignmentId: string, reload?: Function) => {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const archive = () => {
|
||||||
|
// archive assignment
|
||||||
|
setLoading(true);
|
||||||
|
axios
|
||||||
|
.post(`/api/assignments/${assignmentId}/release`)
|
||||||
|
.then((res) => {
|
||||||
|
toast.success("Assignment released!");
|
||||||
|
if (reload) reload();
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("Failed to release the assignment!");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIcon = (downloadClasses: string, loadingClasses: string) => {
|
||||||
|
if (loading) {
|
||||||
|
return <span className={`${loadingClasses} loading loading-infinity w-6`} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tooltip flex items-center justify-center w-fit h-fit"
|
||||||
|
data-tip="Release assignment"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
archive();
|
||||||
|
}}>
|
||||||
|
<BsDoorOpen className={`${downloadClasses} text-2xl cursor-pointer tooltip`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderIcon;
|
||||||
|
};
|
||||||
@@ -10,19 +10,27 @@ interface ModuleResult {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssignmentResult {
|
||||||
|
user: string;
|
||||||
|
type: "academic" | "general";
|
||||||
|
stats: Stat[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Assignment {
|
export interface Assignment {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
assigner: string;
|
assigner: string;
|
||||||
assignees: string[];
|
assignees: string[];
|
||||||
results: {
|
results: AssignmentResult[];
|
||||||
user: string;
|
|
||||||
type: "academic" | "general";
|
|
||||||
stats: Stat[];
|
|
||||||
}[];
|
|
||||||
exams: {id: string; module: Module; assignee: string}[];
|
exams: {id: string; module: Module; assignee: string}[];
|
||||||
instructorGender?: InstructorGender;
|
instructorGender?: InstructorGender;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
released?: boolean;
|
||||||
|
// unless start is active, the assignment is not visible to the assignees
|
||||||
|
// start date now works as a limit time to start the exam
|
||||||
|
start?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AssignmentWithCorporateId = Assignment & { corporateId: string };
|
||||||
|
|||||||
33
src/pages/api/assignments/[id]/release.ts
Normal file
33
src/pages/api/assignments/[id]/release.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { app } from "@/firebase";
|
||||||
|
import { getFirestore, doc, getDoc, setDoc } from "firebase/firestore";
|
||||||
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// verify if it's a logged user that is trying to archive
|
||||||
|
if (req.session.user) {
|
||||||
|
const { id } = req.query as { id: string };
|
||||||
|
const docSnap = await getDoc(doc(db, "assignments", id));
|
||||||
|
|
||||||
|
if (!docSnap.exists()) {
|
||||||
|
res.status(404).json({ ok: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setDoc(docSnap.ref, { released: true }, { merge: true });
|
||||||
|
res.status(200).json({ ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(401).json({ ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "POST") return post(req, res);
|
||||||
|
res.status(404).json({ ok: false });
|
||||||
|
}
|
||||||
46
src/pages/api/assignments/[id]/start.ts
Normal file
46
src/pages/api/assignments/[id]/start.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { app } from "@/firebase";
|
||||||
|
import moment from "moment";
|
||||||
|
import { getFirestore, doc, getDoc, setDoc } from "firebase/firestore";
|
||||||
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
// verify if it's a logged user that is trying to archive
|
||||||
|
if (req.session.user) {
|
||||||
|
const { id } = req.query as { id: string };
|
||||||
|
const docSnap = await getDoc(doc(db, "assignments", id));
|
||||||
|
|
||||||
|
if (!docSnap.exists()) {
|
||||||
|
res.status(404).json({ ok: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = docSnap.data();
|
||||||
|
if (moment().isAfter(moment(data.startDate))) {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ ok: false, message: "Assignmentcan no longer " });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setDoc(
|
||||||
|
docSnap.ref,
|
||||||
|
{ start: true },
|
||||||
|
{ merge: true }
|
||||||
|
);
|
||||||
|
res.status(200).json({ ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(401).json({ ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method === "POST") return post(req, res);
|
||||||
|
res.status(404).json({ ok: false });
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import { sessionOptions } from "@/lib/session";
|
|||||||
import { getAllAssignersByCorporate } from "@/utils/groups.be";
|
import { getAllAssignersByCorporate } from "@/utils/groups.be";
|
||||||
import { getAssignmentsByAssigners } from "@/utils/assignments.be";
|
import { getAssignmentsByAssigners } from "@/utils/assignments.be";
|
||||||
|
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -20,14 +19,49 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { ids } = req.query as { ids: string };
|
const { ids, startDate, endDate } = req.query as {
|
||||||
|
ids: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDateParsed = startDate ? new Date(startDate) : undefined;
|
||||||
|
const endDateParsed = endDate ? new Date(endDate) : undefined;
|
||||||
try {
|
try {
|
||||||
const idsList = ids.split(",");
|
const idsList = ids.split(",");
|
||||||
|
|
||||||
const assigners = await Promise.all(idsList.map(getAllAssignersByCorporate));
|
const assigners = await Promise.all(
|
||||||
const assignmentList = [...assigners.flat(), ...idsList];
|
idsList.map(async (id) => {
|
||||||
const assignments = await getAssignmentsByAssigners(assignmentList);
|
const assigners = await getAllAssignersByCorporate(id);
|
||||||
res.status(200).json(assignments);
|
return {
|
||||||
|
corporateId: id,
|
||||||
|
assigners,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const assignments = await Promise.all(assigners.map(async (data) => {
|
||||||
|
try {
|
||||||
|
const assigners = [...new Set([...data.assigners, data.corporateId])];
|
||||||
|
const assignments = await getAssignmentsByAssigners(
|
||||||
|
assigners,
|
||||||
|
startDateParsed,
|
||||||
|
endDateParsed
|
||||||
|
);
|
||||||
|
return assignments.map((assignment) => ({
|
||||||
|
...assignment,
|
||||||
|
corporateId: data.corporateId,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(assignments);
|
||||||
|
|
||||||
|
// const assignments = await getAssignmentsByAssigners(assignmentList, startDateParsed, endDateParsed);
|
||||||
|
res.status(200).json(assignments.flat());
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {capitalize, flatten, uniqBy} from "lodash";
|
|||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {sendEmail} from "@/email";
|
import {sendEmail} from "@/email";
|
||||||
|
import { release } from "os";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -138,6 +139,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
results: [],
|
results: [],
|
||||||
exams,
|
exams,
|
||||||
instructorGender,
|
instructorGender,
|
||||||
|
released: false,
|
||||||
...body,
|
...body,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,58 @@
|
|||||||
import {app} from "@/firebase";
|
import { app } from "@/firebase";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import {collection, getDocs, getFirestore, query, where} from "firebase/firestore";
|
import {
|
||||||
|
collection,
|
||||||
|
getDocs,
|
||||||
|
getFirestore,
|
||||||
|
query,
|
||||||
|
where,
|
||||||
|
} from "firebase/firestore";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
export const getAssignmentsByAssigner = async (id: string) => {
|
export const getAssignmentsByAssigner = async (
|
||||||
const {docs} = await getDocs(query(collection(db, "assignments"), where("assigner", "==", id)));
|
id: string,
|
||||||
return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
|
startDate?: Date,
|
||||||
|
endDate?: Date
|
||||||
|
) => {
|
||||||
|
const { docs } = await getDocs(
|
||||||
|
query(
|
||||||
|
collection(db, "assignments"),
|
||||||
|
...[
|
||||||
|
where("assigner", "==", id),
|
||||||
|
...(startDate ? [where("startDate", ">=", startDate.toISOString())] : []),
|
||||||
|
// firebase doesnt accept compound queries so we have to filter on the server
|
||||||
|
// ...endDate ? [where("endDate", "<=", endDate)] : [],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (endDate) {
|
||||||
|
return docs
|
||||||
|
.map((x) => ({ ...(x.data() as Assignment), id: x.id }))
|
||||||
|
.filter((x) => new Date(x.endDate) <= endDate) as Assignment[];
|
||||||
|
}
|
||||||
|
return docs.map((x) => ({ ...x.data(), id: x.id })) as Assignment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => {
|
export const getAssignmentsByAssignerBetweenDates = async (
|
||||||
const {docs} = await getDocs(query(collection(db, "assignments"), where("assigner", "==", id), ));
|
id: string,
|
||||||
return docs.map((x) => ({...x.data(), id: x.id})) as Assignment[];
|
startDate: Date,
|
||||||
|
endDate: Date
|
||||||
|
) => {
|
||||||
|
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[]) => {
|
export const getAssignmentsByAssigners = async (
|
||||||
return (await Promise.all(ids.map(getAssignmentsByAssigner))).flat();
|
ids: string[],
|
||||||
|
startDate?: Date,
|
||||||
|
endDate?: Date
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
ids.map((id) => getAssignmentsByAssigner(id, startDate, endDate))
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/utils/assignments.ts
Normal file
18
src/utils/assignments.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
import { Assignment } from "@/interfaces/results";
|
||||||
|
|
||||||
|
export const futureAssignmentFilter = (a: Assignment) =>
|
||||||
|
moment(a.startDate).isAfter(moment()) && !a.archived && !a.start;
|
||||||
|
|
||||||
|
export const pastAssignmentFilter = (a: Assignment) =>
|
||||||
|
(moment(a.endDate).isBefore(moment()) ||
|
||||||
|
a.assignees.length === a.results.length ||
|
||||||
|
(moment(a.startDate).isBefore(moment()) && !a.start)) &&
|
||||||
|
!a.archived;
|
||||||
|
export const archivedAssignmentFilter = (a: Assignment) => a.archived;
|
||||||
|
|
||||||
|
export const activeAssignmentFilter = (a: Assignment) =>
|
||||||
|
moment(a.endDate).isAfter(moment()) &&
|
||||||
|
// && moment(a.startDate).isBefore(moment())
|
||||||
|
a.start &&
|
||||||
|
a.assignees.length > a.results.length;
|
||||||
@@ -128,6 +128,7 @@ export const groupBySession = (stats: Stat[]) => groupBy(stats, "session");
|
|||||||
export const groupByDate = (stats: Stat[]) => groupBy(stats, "date");
|
export const groupByDate = (stats: Stat[]) => groupBy(stats, "date");
|
||||||
export const groupByExam = (stats: Stat[]) => groupBy(stats, "exam");
|
export const groupByExam = (stats: Stat[]) => groupBy(stats, "exam");
|
||||||
export const groupByModule = (stats: Stat[]) => groupBy(stats, "module");
|
export const groupByModule = (stats: Stat[]) => groupBy(stats, "module");
|
||||||
|
export const groupByUser = (stats: Stat[]) => groupBy(stats, "user");
|
||||||
|
|
||||||
export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
|
export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
|
||||||
return stats.map((stat) => ({
|
return stats.map((stat) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user