Added option to start exam which serves as an alternative to start date for the exam
This commit is contained in:
@@ -3,7 +3,15 @@ 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 {
|
||||||
|
BsBook,
|
||||||
|
BsCheckCircle,
|
||||||
|
BsClipboard,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
BsXCircle,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { generate } from "random-words";
|
import { generate } from "random-words";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
@@ -31,17 +39,44 @@ 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);
|
||||||
@@ -50,22 +85,32 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
|
|||||||
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,10 +387,19 @@ 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" },
|
||||||
@@ -263,16 +419,25 @@ 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)
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ 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 {
|
||||||
|
averageLevelCalculator,
|
||||||
|
calculateAverageLevel,
|
||||||
|
calculateBandScore,
|
||||||
|
} from "@/utils/score";
|
||||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
@@ -49,13 +53,27 @@ 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';
|
||||||
|
|
||||||
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>();
|
||||||
@@ -86,35 +104,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",
|
||||||
@@ -123,9 +187,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`,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -139,12 +209,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}
|
||||||
/>
|
/>
|
||||||
@@ -156,7 +226,8 @@ 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 [userBalance, setUserBalance] = useState(0);
|
const [userBalance, setUserBalance] = useState(0);
|
||||||
@@ -165,7 +236,11 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
const { users, reload, isLoading } = useUsers();
|
const { users, reload, isLoading } = useUsers();
|
||||||
const { codes } = useCodes(user.id);
|
const { codes } = useCodes(user.id);
|
||||||
const { groups } = useGroups({ admin: user.id });
|
const { groups } = useGroups({ admin: user.id });
|
||||||
const {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();
|
||||||
@@ -175,9 +250,14 @@ export default function CorporateDashboard({user}: Props) {
|
|||||||
}, [selectedUser, page]);
|
}, [selectedUser, page]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const relatedGroups = groups.filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate");
|
const relatedGroups = groups.filter(
|
||||||
|
(x) =>
|
||||||
|
x.name === "Students" || x.name === "Teachers" || x.name === "Corporate"
|
||||||
|
);
|
||||||
const usersInGroups = relatedGroups.map((x) => x.participants).flat();
|
const usersInGroups = relatedGroups.map((x) => x.participants).flat();
|
||||||
const filteredCodes = codes.filter((x) => !x.userId || !usersInGroups.includes(x.userId));
|
const filteredCodes = codes.filter(
|
||||||
|
(x) => !x.userId || !usersInGroups.includes(x.userId)
|
||||||
|
);
|
||||||
|
|
||||||
setUserBalance(usersInGroups.length + filteredCodes.length);
|
setUserBalance(usersInGroups.length + filteredCodes.length);
|
||||||
}, [codes, groups]);
|
}, [codes, groups]);
|
||||||
@@ -187,16 +267,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>
|
||||||
@@ -222,7 +312,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>
|
||||||
@@ -251,7 +342,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>
|
||||||
@@ -263,18 +355,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} />
|
||||||
@@ -283,12 +379,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
|
||||||
@@ -302,7 +392,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" &&
|
||||||
@@ -311,7 +403,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}
|
||||||
@@ -324,35 +416,52 @@ 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}
|
||||||
@@ -366,9 +475,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}
|
||||||
@@ -383,9 +494,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">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}
|
||||||
@@ -405,7 +518,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",
|
||||||
@@ -417,15 +534,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} />
|
||||||
@@ -443,7 +564,12 @@ 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 } = {
|
||||||
@@ -462,7 +588,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">
|
||||||
@@ -483,26 +613,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={`${userBalance}/${user.corporateInformation?.companyInformation?.userAmount || 0}`}
|
value={`${userBalance}/${
|
||||||
|
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
|
||||||
@@ -515,12 +665,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>
|
||||||
@@ -554,7 +707,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} />
|
||||||
))}
|
))}
|
||||||
@@ -567,7 +724,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} />
|
||||||
@@ -591,7 +749,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",
|
||||||
@@ -601,7 +760,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),
|
||||||
});
|
});
|
||||||
@@ -611,7 +774,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",
|
||||||
@@ -621,7 +785,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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,21 +66,17 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import MasterStatistical from "./MasterStatistical";
|
import MasterStatistical from "./MasterStatistical";
|
||||||
|
import {
|
||||||
|
futureAssignmentFilter,
|
||||||
|
pastAssignmentFilter,
|
||||||
|
archivedAssignmentFilter,
|
||||||
|
activeAssignmentFilter
|
||||||
|
} from '@/utils/assignments';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: MasterCorporateUser;
|
user: MasterCorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
type StudentPerformanceItem = User & {
|
type StudentPerformanceItem = User & {
|
||||||
corporate?: CorporateUser;
|
corporate?: CorporateUser;
|
||||||
@@ -469,7 +465,7 @@ 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(
|
? getCorporateUser(
|
||||||
@@ -606,19 +602,6 @@ 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(
|
.filter(
|
||||||
@@ -727,11 +710,11 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<span>
|
<span>
|
||||||
<b>Total:</b>{" "}
|
<b>Total:</b>{" "}
|
||||||
{assignments
|
{assignments
|
||||||
.filter(activeFilter)
|
.filter(activeAssignmentFilter)
|
||||||
.reduce((acc, curr) => acc + curr.results.length, 0)}
|
.reduce((acc, curr) => acc + curr.results.length, 0)}
|
||||||
/
|
/
|
||||||
{assignments
|
{assignments
|
||||||
.filter(activeFilter)
|
.filter(activeAssignmentFilter)
|
||||||
.reduce((acc, curr) => curr.exams.length + acc, 0)}
|
.reduce((acc, curr) => curr.exams.length + acc, 0)}
|
||||||
</span>
|
</span>
|
||||||
{Object.keys(
|
{Object.keys(
|
||||||
@@ -761,10 +744,10 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">
|
||||||
Active Assignments ({assignments.filter(activeFilter).length})
|
Active Assignments ({assignments.filter(activeAssignmentFilter).length})
|
||||||
</h2>
|
</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
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
@@ -776,7 +759,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">
|
||||||
Planned Assignments ({assignments.filter(futureFilter).length})
|
Planned Assignments ({assignments.filter(futureAssignmentFilter).length})
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
@@ -786,7 +769,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
<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}
|
||||||
@@ -801,10 +784,10 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">
|
||||||
Past Assignments ({assignments.filter(pastFilter).length})
|
Past Assignments ({assignments.filter(pastAssignmentFilter).length})
|
||||||
</h2>
|
</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}
|
||||||
@@ -820,10 +803,10 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">
|
<h2 className="text-2xl font-semibold">
|
||||||
Archived Assignments ({assignments.filter(archivedFilter).length})
|
Archived Assignments ({assignments.filter(archivedAssignmentFilter).length})
|
||||||
</h2>
|
</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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ 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;
|
||||||
@@ -59,13 +65,18 @@ export default function TeacherDashboard({user}: Props) {
|
|||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
const [corporateUserToShow, setCorporateUserToShow] = 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,7 +173,12 @@ 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 } = {
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export interface Assignment {
|
|||||||
endDate: Date;
|
endDate: Date;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
released?: 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 };
|
export type AssignmentWithCorporateId = Assignment & { corporateId: string };
|
||||||
|
|||||||
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 });
|
||||||
|
}
|
||||||
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;
|
||||||
Reference in New Issue
Block a user