Merge remote-tracking branch 'origin/develop' into feature/level-file-upload

This commit is contained in:
Carlos Mesquita
2024-09-06 09:41:01 +01:00
13 changed files with 282 additions and 284 deletions

View File

@@ -40,61 +40,71 @@ 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">
<span className="flex gap-1"> <div className="flex flex-col gap-3">
<b>ID:</b> <span className="flex gap-1">
{session.sessionId} <b>ID:</b>
</span> {session.sessionId}
<span className="flex gap-1"> </span>
<b>Date:</b> <span className="flex gap-1">
{moment(session.date).format("DD/MM/YYYY - HH:mm")} <b>Date:</b>
</span> {moment(session.date).format("DD/MM/YYYY - HH:mm")}
<div className="flex w-full items-center justify-between"> </span>
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-center justify-center gap-2"> {session.assignment && (
{session.selectedModules.sort(sortByModuleName).map((module) => ( <span className="flex flex-col gap-0">
<div <b>Assignment:</b>
key={module} {session.assignment.name}
data-tip={capitalize(module)} </span>
className={clsx( )}
"-md:px-4 tooltip flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
module === "reading" && "bg-ielts-reading",
module === "listening" && "bg-ielts-listening",
module === "writing" && "bg-ielts-writing",
module === "speaking" && "bg-ielts-speaking",
module === "level" && "bg-ielts-level",
)}>
{module === "reading" && <BsBook className="h-4 w-4" />}
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
{module === "writing" && <BsPen className="h-4 w-4" />}
{module === "speaking" && <BsMegaphone className="h-4 w-4" />}
{module === "level" && <BsClipboard className="h-4 w-4" />}
</div>
))}
</div>
</div> </div>
<div className="flex items-center gap-2 w-full"> <div className="flex flex-col gap-3">
<button <div className="flex w-full items-center justify-between">
onClick={async () => await loadSession(session)} <div className="-md:mt-2 grid w-full grid-cols-4 place-items-center justify-center gap-2">
disabled={isLoading} {session.selectedModules.sort(sortByModuleName).map((module) => (
className="bg-mti-green-ultralight w-full hover:bg-mti-green-light rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"> <div
{!isLoading && "Resume"} key={module}
{isLoading && ( data-tip={capitalize(module)}
<div className="flex items-center justify-center"> className={clsx(
<BsArrowRepeat className="animate-spin text-white" size={25} /> "-md:px-4 tooltip flex w-fit items-center gap-2 rounded-xl py-2 text-white md:px-2 xl:px-4",
</div> module === "reading" && "bg-ielts-reading",
)} module === "listening" && "bg-ielts-listening",
</button> module === "writing" && "bg-ielts-writing",
<button module === "speaking" && "bg-ielts-speaking",
onClick={deleteSession} module === "level" && "bg-ielts-level",
disabled={isLoading} )}>
className="bg-mti-red-ultralight w-full hover:bg-mti-red-light rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed"> {module === "reading" && <BsBook className="h-4 w-4" />}
{!isLoading && "Delete"} {module === "listening" && <BsHeadphones className="h-4 w-4" />}
{isLoading && ( {module === "writing" && <BsPen className="h-4 w-4" />}
<div className="flex items-center justify-center"> {module === "speaking" && <BsMegaphone className="h-4 w-4" />}
<BsArrowRepeat className="animate-spin text-white" size={25} /> {module === "level" && <BsClipboard className="h-4 w-4" />}
</div> </div>
)} ))}
</button> </div>
</div>
<div className="flex items-center gap-2 w-full">
<button
onClick={async () => await loadSession(session)}
disabled={isLoading}
className="bg-mti-green-ultralight w-full hover:bg-mti-green-light rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed">
{!isLoading && "Resume"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
<button
onClick={deleteSession}
disabled={isLoading}
className="bg-mti-red-ultralight w-full hover:bg-mti-red-light rounded-lg p-2 px-4 transition duration-300 ease-in-out hover:text-white disabled:cursor-not-allowed">
{!isLoading && "Delete"}
{isLoading && (
<div className="flex items-center justify-center">
<BsArrowRepeat className="animate-spin text-white" size={25} />
</div>
)}
</button>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -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}),

View File

@@ -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

View File

@@ -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}

View File

@@ -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>
<Button <div
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl" data-tip="You have already started this assignment!"
onClick={() => startAssignment(assignment)} className={clsx(
variant="outline"> "-md:hidden h-full w-full max-w-[50%] cursor-pointer",
Start sessions.filter((x) => x.assignment?.id === assignment.id).length > 0 && "tooltip",
</Button> )}>
<Button
className={clsx("w-full h-full !rounded-xl")}
onClick={() => startAssignment(assignment)}
variant="outline"
disabled={sessions.filter((x) => x.assignment?.id === assignment.id).length > 0}>
Start
</Button>
</div>
</> </>
)} )}
{assignment.results.map((r) => r.user).includes(user.id) && ( {assignment.results.map((r) => r.user).includes(user.id) && (

View File

@@ -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

View File

@@ -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">

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) => ({
id: doc.id,
...doc.data(),
})) as Session[];
res.status(200).json( res.status(200).json(
snapshot.docs.map((doc) => ({ sessions.filter((x) => {
id: doc.id, if (!x.assignment) return true;
...doc.data(), if (x.assignment.results.filter((y) => y.user === user).length > 0) return false;
})), return !moment().isAfter(moment(x.assignment.endDate));
}),
); );
} }

View File

@@ -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) => ({
id: doc.id,
...doc.data(),
}));
res.status(200).json( if (!req.session.user) return res.status(200).json(users);
snapshot.docs.map((doc) => ({ if (req.session.user.type === "admin" || req.session.user.type === "developer") return res.status(200).json(users);
id: doc.id,
...doc.data(), 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)));
} }

View File

@@ -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;