Merge remote-tracking branch 'origin/develop' into feature/level-file-upload
This commit is contained in:
@@ -40,7 +40,8 @@ export default function SessionCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-mti-gray-anti-flash flex w-64 flex-col gap-3 rounded-xl border p-4 text-black">
|
<div className="border-mti-gray-anti-flash flex w-64 flex-col justify-between gap-3 rounded-xl border p-4 text-black">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
<b>ID:</b>
|
<b>ID:</b>
|
||||||
{session.sessionId}
|
{session.sessionId}
|
||||||
@@ -49,6 +50,14 @@ export default function SessionCard({
|
|||||||
<b>Date:</b>
|
<b>Date:</b>
|
||||||
{moment(session.date).format("DD/MM/YYYY - HH:mm")}
|
{moment(session.date).format("DD/MM/YYYY - HH:mm")}
|
||||||
</span>
|
</span>
|
||||||
|
{session.assignment && (
|
||||||
|
<span className="flex flex-col gap-0">
|
||||||
|
<b>Assignment:</b>
|
||||||
|
{session.assignment.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-center justify-center gap-2">
|
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-center justify-center gap-2">
|
||||||
{session.selectedModules.sort(sortByModuleName).map((module) => (
|
{session.selectedModules.sort(sortByModuleName).map((module) => (
|
||||||
@@ -97,5 +106,6 @@ export default function SessionCard({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,11 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
|
|||||||
selectedTrainingExams.some((exam) => exam.includes(timestamp)) &&
|
selectedTrainingExams.some((exam) => exam.includes(timestamp)) &&
|
||||||
"border-2 border-slate-600",
|
"border-2 border-slate-600",
|
||||||
)}
|
)}
|
||||||
onClick={examNumber === undefined ? selectExam : undefined}
|
onClick={() => {
|
||||||
|
if (!!assignment && !assignment.released) return;
|
||||||
|
if (examNumber === undefined) return selectExam();
|
||||||
|
return;
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
...(width !== undefined && {width}),
|
...(width !== undefined && {width}),
|
||||||
...(height !== undefined && {height}),
|
...(height !== undefined && {height}),
|
||||||
|
|||||||
@@ -207,64 +207,6 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const StudentsList = () => {
|
|
||||||
const filter = (x: User) =>
|
|
||||||
x.type === "student" &&
|
|
||||||
(!!selectedUser
|
|
||||||
? groups
|
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(x.id) || false
|
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserList
|
|
||||||
user={user}
|
|
||||||
filters={[filter]}
|
|
||||||
renderHeader={(total) => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
|
||||||
<BsArrowLeft className="text-xl" />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TeachersList = () => {
|
|
||||||
const filter = (x: User) =>
|
|
||||||
x.type === "teacher" &&
|
|
||||||
(!!selectedUser
|
|
||||||
? groups
|
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(x.id) || false
|
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserList
|
|
||||||
user={user}
|
|
||||||
filters={[filter]}
|
|
||||||
renderHeader={(total) => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
|
||||||
<BsArrowLeft className="text-xl" />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const 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);
|
||||||
|
|
||||||
@@ -518,8 +460,40 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
{router.asPath === "/#students" && <StudentsList />}
|
{router.asPath === "/#students" && (
|
||||||
{router.asPath === "/#teachers" && <TeachersList />}
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "student"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{router.asPath === "/#teachers" && (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "teacher"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{router.asPath === "/#groups" && <GroupsList />}
|
{router.asPath === "/#groups" && <GroupsList />}
|
||||||
{router.asPath === "/#assignments" && (
|
{router.asPath === "/#assignments" && (
|
||||||
<AssignmentsPage
|
<AssignmentsPage
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
);
|
);
|
||||||
}, [assignments, groups, users]);
|
}, [assignments, groups, users]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id);
|
const studentFilter = (user: User) => user.type === "student";
|
||||||
const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id);
|
const teacherFilter = (user: User) => user.type === "teacher";
|
||||||
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) => (
|
||||||
@@ -365,74 +365,7 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const StudentsList = () => {
|
const corporateUserFilter = (x: User) => x.type === "corporate";
|
||||||
const filter = (x: User) =>
|
|
||||||
x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserList
|
|
||||||
user={user}
|
|
||||||
filters={[filter]}
|
|
||||||
renderHeader={(total) => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
|
||||||
<BsArrowLeft className="text-xl" />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TeachersList = () => {
|
|
||||||
const filter = (x: User) =>
|
|
||||||
x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserList
|
|
||||||
user={user}
|
|
||||||
filters={[filter]}
|
|
||||||
renderHeader={(total) => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
|
||||||
<BsArrowLeft className="text-xl" />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const corporateUserFilter = (x: User) =>
|
|
||||||
x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id));
|
|
||||||
|
|
||||||
const CorporateList = () => {
|
|
||||||
return (
|
|
||||||
<UserList
|
|
||||||
user={user}
|
|
||||||
filters={[corporateUserFilter]}
|
|
||||||
renderHeader={(total) => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
|
||||||
<BsArrowLeft className="text-xl" />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold">Corporates ({total})</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GroupsList = () => {
|
const GroupsList = () => {
|
||||||
return (
|
return (
|
||||||
@@ -704,10 +637,58 @@ export default function MasterCorporateDashboard({user}: Props) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
{router.asPath === "/#students" && <StudentsList />}
|
{router.asPath === "/#students" && (
|
||||||
{router.asPath === "/#teachers" && <TeachersList />}
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "student"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{router.asPath === "/#teachers" && (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "teacher"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Teachers ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{router.asPath === "/#groups" && <GroupsList />}
|
{router.asPath === "/#groups" && <GroupsList />}
|
||||||
{router.asPath === "/#corporate" && <CorporateList />}
|
{router.asPath === "/#corporate" && (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "corporate"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Corporate ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{router.asPath === "/#assignments" && (
|
{router.asPath === "/#assignments" && (
|
||||||
<AssignmentsPage
|
<AssignmentsPage
|
||||||
assignments={assignments}
|
assignments={assignments}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsM
|
|||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {activeAssignmentFilter} from "@/utils/assignments";
|
import {activeAssignmentFilter} from "@/utils/assignments";
|
||||||
import ModuleBadge from "@/components/ModuleBadge";
|
import ModuleBadge from "@/components/ModuleBadge";
|
||||||
|
import useSessions from "@/hooks/useSessions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -38,6 +39,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function StudentDashboard({user, users, linkedCorporate}: Props) {
|
export default function StudentDashboard({user, users, linkedCorporate}: Props) {
|
||||||
const {gradingSystem} = useGradingSystem();
|
const {gradingSystem} = useGradingSystem();
|
||||||
|
const {sessions} = useSessions(user.id);
|
||||||
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id, !user?.id);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id, !user?.id);
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
|
||||||
@@ -160,12 +162,20 @@ export default function StudentDashboard({user, users, linkedCorporate}: Props)
|
|||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
data-tip="You have already started this assignment!"
|
||||||
|
className={clsx(
|
||||||
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
||||||
|
sessions.filter((x) => x.assignment?.id === assignment.id).length > 0 && "tooltip",
|
||||||
|
)}>
|
||||||
<Button
|
<Button
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className={clsx("w-full h-full !rounded-xl")}
|
||||||
onClick={() => startAssignment(assignment)}
|
onClick={() => startAssignment(assignment)}
|
||||||
variant="outline">
|
variant="outline"
|
||||||
|
disabled={sessions.filter((x) => x.assignment?.id === assignment.id).length > 0}>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{assignment.results.map((r) => r.user).includes(user.id) && (
|
{assignment.results.map((r) => r.user).includes(user.id) && (
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import usePermissions from "@/hooks/usePermissions";
|
|||||||
import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
|
import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments";
|
||||||
import AssignmentsPage from "./views/AssignmentsPage";
|
import AssignmentsPage from "./views/AssignmentsPage";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -67,6 +68,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
|
|||||||
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});
|
||||||
|
|
||||||
|
const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
|
const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]);
|
||||||
@@ -90,7 +92,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
|
|||||||
setShowModal(!!selectedUser && router.asPath === "/#");
|
setShowModal(!!selectedUser && router.asPath === "/#");
|
||||||
}, [selectedUser, router.asPath]);
|
}, [selectedUser, router.asPath]);
|
||||||
|
|
||||||
const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id);
|
const studentFilter = (user: User) => user.type === "student";
|
||||||
|
|
||||||
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id);
|
||||||
|
|
||||||
@@ -106,35 +108,6 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const StudentsList = () => {
|
|
||||||
const filter = (x: User) =>
|
|
||||||
x.type === "student" &&
|
|
||||||
(!!selectedUser
|
|
||||||
? groups
|
|
||||||
.filter((g) => g.admin === selectedUser.id)
|
|
||||||
.flatMap((g) => g.participants)
|
|
||||||
.includes(x.id) || false
|
|
||||||
: groups.flatMap((g) => g.participants).includes(x.id));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserList
|
|
||||||
user={user}
|
|
||||||
filters={[filter]}
|
|
||||||
renderHeader={(total) => (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
|
||||||
<BsArrowLeft className="text-xl" />
|
|
||||||
<span>Back</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GroupsList = () => {
|
const GroupsList = () => {
|
||||||
const filter = (x: Group) => x.admin === user.id;
|
const filter = (x: Group) => x.admin === user.id;
|
||||||
|
|
||||||
@@ -285,16 +258,68 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) {
|
|||||||
if (shouldReload) reload();
|
if (shouldReload) reload();
|
||||||
}}
|
}}
|
||||||
onViewStudents={
|
onViewStudents={
|
||||||
selectedUser.type === "corporate" || selectedUser.type === "teacher" ? () => router.push("/#students") : undefined
|
selectedUser.type === "corporate" || selectedUser.type === "teacher"
|
||||||
|
? () => {
|
||||||
|
appendUserFilters({
|
||||||
|
id: "view-students",
|
||||||
|
filter: (x: User) => x.type === "student",
|
||||||
|
});
|
||||||
|
appendUserFilters({
|
||||||
|
id: "belongs-to-admin",
|
||||||
|
filter: (x: User) =>
|
||||||
|
groups
|
||||||
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/list/users");
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onViewTeachers={
|
||||||
|
selectedUser.type === "corporate" || selectedUser.type === "student"
|
||||||
|
? () => {
|
||||||
|
appendUserFilters({
|
||||||
|
id: "view-teachers",
|
||||||
|
filter: (x: User) => x.type === "teacher",
|
||||||
|
});
|
||||||
|
appendUserFilters({
|
||||||
|
id: "belongs-to-admin",
|
||||||
|
filter: (x: User) =>
|
||||||
|
groups
|
||||||
|
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id))
|
||||||
|
.flatMap((g) => g.participants)
|
||||||
|
.includes(x.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/list/users");
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onViewTeachers={selectedUser.type === "corporate" ? () => router.push("/#teachers") : undefined}
|
|
||||||
user={selectedUser}
|
user={selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
{router.asPath === "/#students" && <StudentsList />}
|
{router.asPath === "/#students" && (
|
||||||
|
<UserList
|
||||||
|
user={user}
|
||||||
|
filters={[(x) => x.type === "student"]}
|
||||||
|
renderHeader={(total) => (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300">
|
||||||
|
<BsArrowLeft className="text-xl" />
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold">Students ({total})</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{router.asPath === "/#groups" && <GroupsList />}
|
{router.asPath === "/#groups" && <GroupsList />}
|
||||||
{router.asPath === "/#assignments" && (
|
{router.asPath === "/#assignments" && (
|
||||||
<AssignmentsPage
|
<AssignmentsPage
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export default function Finish({user, scores, modules, information, solutions, i
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{assignment && !assignment.released && (
|
{assignment && !assignment.released && !isLoading && (
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-12">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-12">
|
||||||
{/* <span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} /> */}
|
{/* <span className={clsx("loading loading-infinity w-32", moduleColors[selectedModule].progress)} /> */}
|
||||||
<BsBan size={64} className={clsx(moduleColors[selectedModule].progress)} />
|
<BsBan size={64} className={clsx(moduleColors[selectedModule].progress)} />
|
||||||
@@ -223,7 +223,7 @@ export default function Finish({user, scores, modules, information, solutions, i
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && !(assignment && !assignment.released) && (
|
||||||
<div className="mb-20 mt-32 flex w-full items-center justify-between gap-9">
|
<div className="mb-20 mt-32 flex w-full items-center justify-between gap-9">
|
||||||
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
||||||
<div className="flex gap-9 px-16">
|
<div className="flex gap-9 px-16">
|
||||||
|
|||||||
@@ -80,19 +80,15 @@ export default function UserList({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (user && users) {
|
if (users) {
|
||||||
const filterUsers = ["corporate", "teacher", "mastercorporate"].includes(user.type)
|
const filteredUsers = filters.reduce((d, f) => d.filter(f), users);
|
||||||
? users.filter((u) => groups.flatMap((g) => g.participants).includes(u.id))
|
|
||||||
: users;
|
|
||||||
|
|
||||||
const filteredUsers = filters.reduce((d, f) => d.filter(f), filterUsers);
|
|
||||||
const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
|
const sortedUsers = await asyncSorter<User>(filteredUsers, sortFunction);
|
||||||
|
|
||||||
setDisplayUsers([...sortedUsers]);
|
setDisplayUsers([...sortedUsers]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user, users, sorter, groups]);
|
}, [users, sorter]);
|
||||||
|
|
||||||
const deleteAccount = (user: User) => {
|
const deleteAccount = (user: User) => {
|
||||||
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
if (!confirm(`Are you sure you want to delete ${user.name}'s account?`)) return;
|
||||||
|
|||||||
@@ -30,30 +30,10 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
participant: string;
|
participant: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (req.session?.user?.type === "mastercorporate") {
|
const adminGroups = await getGroupsForUser(admin, participant);
|
||||||
try {
|
const participants = uniq(adminGroups.flatMap((g) => g.participants));
|
||||||
const masterCorporateGroups = await getGroupsForUser(admin, participant);
|
const groups = await Promise.all(participants.map(async (c) => await getGroupsForUser(c, participant)));
|
||||||
const participants = uniq(masterCorporateGroups.flatMap((g) => g.participants));
|
return res.status(200).json([...adminGroups, ...uniqBy(groups.flat(), "id")]);
|
||||||
const corporatesFromMaster = (await Promise.all(participants.map(getUser))).filter((x) => x.type === "corporate");
|
|
||||||
|
|
||||||
if (corporatesFromMaster.length === 0) return res.status(200).json(masterCorporateGroups);
|
|
||||||
|
|
||||||
const groups = await Promise.all(corporatesFromMaster.map((c) => getGroupsForUser(c.id, participant)));
|
|
||||||
return res.status(200).json([...masterCorporateGroups, ...uniqBy(groups.flat(), "id")]);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
res.status(500).json({ok: false});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const groups = await getGroupsForUser(admin, participant);
|
|
||||||
res.status(200).json(groups);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
res.status(500).json({ok: false});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, de
|
|||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {v4} from "uuid";
|
import {v4} from "uuid";
|
||||||
import {CorporateUser, Group} from "@/interfaces/user";
|
import {CorporateUser, Group, Type} from "@/interfaces/user";
|
||||||
import {createUserWithEmailAndPassword, getAuth} from "firebase/auth";
|
import {createUserWithEmailAndPassword, getAuth} from "firebase/auth";
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import {getUserCorporate} from "@/utils/groups.be";
|
import {getUserCorporate, getUserGroups} from "@/utils/groups.be";
|
||||||
|
import {uniq} from "lodash";
|
||||||
|
import {getUser} from "@/utils/users.be";
|
||||||
|
|
||||||
const DEFAULT_DESIRED_LEVELS = {
|
const DEFAULT_DESIRED_LEVELS = {
|
||||||
reading: 9,
|
reading: 9,
|
||||||
@@ -28,6 +30,13 @@ const db = getFirestore(app);
|
|||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
const getUsersOfType = async (admin: string, type: Type) => {
|
||||||
|
const groups = await getUserGroups(admin);
|
||||||
|
const users = await Promise.all(uniq(groups.flatMap((x) => x.participants)).map(getUser));
|
||||||
|
|
||||||
|
return users.filter((x) => x.type === type).map((x) => x.id);
|
||||||
|
};
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method === "POST") return post(req, res);
|
if (req.method === "POST") return post(req, res);
|
||||||
|
|
||||||
@@ -106,11 +115,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (type === "corporate") {
|
if (type === "corporate") {
|
||||||
|
const students = maker.type === "corporate" ? await getUsersOfType(maker.id, "student") : [];
|
||||||
|
const teachers = maker.type === "corporate" ? await getUsersOfType(maker.id, "teacher") : [];
|
||||||
|
|
||||||
const defaultTeachersGroup: Group = {
|
const defaultTeachersGroup: Group = {
|
||||||
admin: userId,
|
admin: userId,
|
||||||
id: v4(),
|
id: v4(),
|
||||||
name: "Teachers",
|
name: "Teachers",
|
||||||
participants: [],
|
participants: teachers,
|
||||||
disableEditing: true,
|
disableEditing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,21 +130,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
admin: userId,
|
admin: userId,
|
||||||
id: v4(),
|
id: v4(),
|
||||||
name: "Students",
|
name: "Students",
|
||||||
participants: [],
|
participants: students,
|
||||||
disableEditing: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultCorporateGroup: Group = {
|
|
||||||
admin: userId,
|
|
||||||
id: v4(),
|
|
||||||
name: "Corporate",
|
|
||||||
participants: [],
|
|
||||||
disableEditing: true,
|
disableEditing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
|
await setDoc(doc(db, "groups", defaultTeachersGroup.id), defaultTeachersGroup);
|
||||||
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
|
await setDoc(doc(db, "groups", defaultStudentsGroup.id), defaultStudentsGroup);
|
||||||
await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!corporate) {
|
if (!!corporate) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {app} from "@/firebase";
|
|||||||
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore";
|
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {Session} from "@/hooks/useSessions";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -24,12 +26,17 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const q = user ? query(collection(db, "sessions"), where("user", "==", user)) : collection(db, "sessions");
|
const q = user ? query(collection(db, "sessions"), where("user", "==", user)) : collection(db, "sessions");
|
||||||
const snapshot = await getDocs(q);
|
const snapshot = await getDocs(q);
|
||||||
|
const sessions = snapshot.docs.map((doc) => ({
|
||||||
res.status(200).json(
|
|
||||||
snapshot.docs.map((doc) => ({
|
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})),
|
})) as Session[];
|
||||||
|
|
||||||
|
res.status(200).json(
|
||||||
|
sessions.filter((x) => {
|
||||||
|
if (!x.assignment) return true;
|
||||||
|
if (x.assignment.results.filter((y) => y.user === user).length > 0) return false;
|
||||||
|
return !moment().isAfter(moment(x.assignment.endDate));
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {app} from "@/firebase";
|
|||||||
import {getFirestore, collection, getDocs} from "firebase/firestore";
|
import {getFirestore, collection, getDocs} from "firebase/firestore";
|
||||||
import {withIronSessionApiRoute} from "iron-session/next";
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
import {getGroupsForUser} from "@/utils/groups.be";
|
||||||
|
import {uniq} from "lodash";
|
||||||
|
|
||||||
const db = getFirestore(app);
|
const db = getFirestore(app);
|
||||||
|
|
||||||
@@ -16,11 +18,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = await getDocs(collection(db, "users"));
|
const snapshot = await getDocs(collection(db, "users"));
|
||||||
|
const users = snapshot.docs.map((doc) => ({
|
||||||
res.status(200).json(
|
|
||||||
snapshot.docs.map((doc) => ({
|
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
...doc.data(),
|
...doc.data(),
|
||||||
})),
|
}));
|
||||||
);
|
|
||||||
|
if (!req.session.user) return res.status(200).json(users);
|
||||||
|
if (req.session.user.type === "admin" || req.session.user.type === "developer") return res.status(200).json(users);
|
||||||
|
|
||||||
|
const adminGroups = await getGroupsForUser(req.session.user.id);
|
||||||
|
const groups = await Promise.all(adminGroups.flatMap((x) => x.participants).map(async (x) => await getGroupsForUser(x)));
|
||||||
|
const participants = uniq([...adminGroups.flatMap((x) => x.participants), ...groups.flat().flatMap((x) => x.participants)]);
|
||||||
|
|
||||||
|
res.status(200).json(users.filter((x) => participants.includes(x.id)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const getUserCorporate = async (id: string) => {
|
|||||||
|
|
||||||
const groups = await getParticipantGroups(id);
|
const groups = await getParticipantGroups(id);
|
||||||
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
|
const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
|
||||||
const corporates = admins.filter((x) => x.type === "corporate" || x.type === "mastercorporate");
|
const corporates = admins.filter((x) => (user.type === "corporate" ? x.type === "mastercorporate" : x.type === "corporate"));
|
||||||
|
|
||||||
if (corporates.length === 0) return undefined;
|
if (corporates.length === 0) return undefined;
|
||||||
return corporates.shift() as CorporateUser | MasterCorporateUser;
|
return corporates.shift() as CorporateUser | MasterCorporateUser;
|
||||||
|
|||||||
Reference in New Issue
Block a user