Merged in refactor-getserverprops (pull request #142)
Refactor most getServerProps to make independent requests in parallel and projected the data only to return the necessary fields and changed some functions Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import { Session } from "@/hooks/useSessions";
|
import { Session } from "@/hooks/useSessions";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments";
|
import {
|
||||||
|
activeAssignmentFilter,
|
||||||
|
futureAssignmentFilter,
|
||||||
|
} from "@/utils/assignments";
|
||||||
import { sortByModuleName } from "@/utils/moduleUtils";
|
import { sortByModuleName } from "@/utils/moduleUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -11,102 +14,124 @@ import Button from "../Low/Button";
|
|||||||
import ModuleBadge from "../ModuleBadge";
|
import ModuleBadge from "../ModuleBadge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assignment: Assignment
|
assignment: Assignment;
|
||||||
user: User
|
user: User;
|
||||||
session?: Session
|
session?: Session;
|
||||||
startAssignment: (assignment: Assignment) => void
|
startAssignment: (assignment: Assignment) => void;
|
||||||
resumeAssignment: (session: Session) => void
|
resumeAssignment: (session: Session) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) {
|
export default function AssignmentCard({
|
||||||
const router = useRouter()
|
user,
|
||||||
|
assignment,
|
||||||
|
session,
|
||||||
|
startAssignment,
|
||||||
|
resumeAssignment,
|
||||||
|
}: Props) {
|
||||||
|
const hasBeenSubmitted = useMemo(
|
||||||
|
() => assignment.results.map((r) => r.user).includes(user.id),
|
||||||
|
[assignment.results, user.id]
|
||||||
|
);
|
||||||
|
|
||||||
const hasBeenSubmitted = useMemo(() => assignment.results.map((r) => r.user).includes(user.id), [assignment.results, user.id])
|
return (
|
||||||
|
<div
|
||||||
return (
|
className={clsx(
|
||||||
<div
|
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
||||||
className={clsx(
|
assignment.results.map((r) => r.user).includes(user.id) &&
|
||||||
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
"border-mti-green-light"
|
||||||
assignment.results.map((r) => r.user).includes(user.id) && "border-mti-green-light",
|
)}
|
||||||
)}
|
key={assignment.id}
|
||||||
key={assignment.id}>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="text-mti-black/90 text-xl font-semibold">{assignment.name}</h3>
|
<h3 className="text-mti-black/90 text-xl font-semibold">
|
||||||
<span className="flex justify-between gap-1 text-lg">
|
{assignment.name}
|
||||||
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
|
</h3>
|
||||||
<span>-</span>
|
<span className="flex justify-between gap-1 text-lg">
|
||||||
<span>{moment(assignment.endDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
</span>
|
<span>-</span>
|
||||||
</div>
|
<span>{moment(assignment.endDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
<div className="flex w-full items-center justify-between">
|
</span>
|
||||||
<div className="-md:mt-2 grid w-fit min-w-[140px] grid-cols-2 grid-rows-2 place-items-center justify-between gap-4">
|
</div>
|
||||||
{assignment.exams
|
<div className="flex w-full items-center justify-between">
|
||||||
.filter((e) => e.assignee === user.id)
|
<div className="-md:mt-2 grid w-fit min-w-[140px] grid-cols-2 grid-rows-2 place-items-center justify-between gap-4">
|
||||||
.map((e) => e.module)
|
{assignment.exams
|
||||||
.sort(sortByModuleName)
|
.filter((e) => e.assignee === user.id)
|
||||||
.map((module) => (
|
.map((e) => e.module)
|
||||||
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
.sort(sortByModuleName)
|
||||||
))}
|
.map((module) => (
|
||||||
</div>
|
<ModuleBadge
|
||||||
{futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
className="scale-110 w-full"
|
||||||
<Button
|
key={module}
|
||||||
color="rose"
|
module={module}
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
/>
|
||||||
disabled
|
))}
|
||||||
variant="outline">
|
</div>
|
||||||
Not yet started
|
{futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
||||||
</Button>
|
<Button
|
||||||
)}
|
color="rose"
|
||||||
{activeAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
<>
|
disabled
|
||||||
<div
|
variant="outline"
|
||||||
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">
|
Not yet started
|
||||||
<Button className="h-full w-full !rounded-xl" variant="outline">
|
</Button>
|
||||||
Start
|
)}
|
||||||
</Button>
|
{activeAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
||||||
</div>
|
<>
|
||||||
{!session && (
|
<div
|
||||||
<div
|
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
||||||
data-tip="You have already started this assignment!"
|
data-tip="Your screen size is too small to perform an assignment"
|
||||||
className={clsx(
|
>
|
||||||
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
<Button className="h-full w-full !rounded-xl" variant="outline">
|
||||||
!!session && "tooltip",
|
Start
|
||||||
)}>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
className={clsx("w-full h-full !rounded-xl")}
|
{!session && (
|
||||||
onClick={() => startAssignment(assignment)}
|
<div
|
||||||
variant="outline">
|
data-tip="You have already started this assignment!"
|
||||||
Start
|
className={clsx(
|
||||||
</Button>
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
||||||
</div>
|
!!session && "tooltip"
|
||||||
)}
|
)}
|
||||||
{!!session && (
|
>
|
||||||
<div
|
<Button
|
||||||
className={clsx(
|
className={clsx("w-full h-full !rounded-xl")}
|
||||||
"-md:hidden h-full w-full max-w-[50%] cursor-pointer"
|
onClick={() => startAssignment(assignment)}
|
||||||
)}>
|
variant="outline"
|
||||||
<Button
|
>
|
||||||
className={clsx("w-full h-full !rounded-xl")}
|
Start
|
||||||
onClick={() => resumeAssignment(session)}
|
</Button>
|
||||||
color="green"
|
</div>
|
||||||
variant="outline">
|
)}
|
||||||
Resume
|
{!!session && (
|
||||||
</Button>
|
<div
|
||||||
</div>
|
className={clsx(
|
||||||
)}
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer"
|
||||||
</>
|
)}
|
||||||
)}
|
>
|
||||||
{hasBeenSubmitted && (
|
<Button
|
||||||
<Button
|
className={clsx("w-full h-full !rounded-xl")}
|
||||||
color="green"
|
onClick={() => resumeAssignment(session)}
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
color="green"
|
||||||
disabled
|
variant="outline"
|
||||||
variant="outline">
|
>
|
||||||
Submitted
|
Resume
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</>
|
||||||
)
|
)}
|
||||||
|
{hasBeenSubmitted && (
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
|
disabled
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Submitted
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,14 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
|
|
||||||
return pageProps?.user ? (
|
return pageProps?.user ? (
|
||||||
<Layout user={pageProps.user} entities={entities} refreshPage={loading}>
|
<Layout user={pageProps.user} entities={entities} refreshPage={loading}>
|
||||||
{loading ? <UserProfileSkeleton /> : <Component {...pageProps} />}
|
{loading ? (
|
||||||
|
// TODO: Change this later to a better loading screen (example: skeletons for each page)
|
||||||
|
<div className="min-h-screen flex justify-center items-start">
|
||||||
|
<span className="loading loading-infinity w-32" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Component entities={entities} {...pageProps} />
|
||||||
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
) : (
|
) : (
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,16 @@ import { useListSearch } from "@/hooks/useListSearch";
|
|||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
activeAssignmentFilter,
|
activeAssignmentFilter,
|
||||||
archivedAssignmentFilter,
|
archivedAssignmentFilter,
|
||||||
futureAssignmentFilter,
|
futureAssignmentFilter,
|
||||||
pastAssignmentFilter,
|
pastAssignmentFilter,
|
||||||
startHasExpiredAssignmentFilter,
|
startHasExpiredAssignmentFilter,
|
||||||
} from "@/utils/assignments";
|
} from "@/utils/assignments";
|
||||||
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
@@ -28,197 +28,309 @@ import { useMemo } from "react";
|
|||||||
import { BsChevronLeft, BsPlus } from "react-icons/bs";
|
import { BsChevronLeft, BsPlus } from "react-icons/bs";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"]))
|
if (
|
||||||
return redirect("/")
|
!checkAccess(user, [
|
||||||
|
"admin",
|
||||||
|
"developer",
|
||||||
|
"corporate",
|
||||||
|
"teacher",
|
||||||
|
"mastercorporate",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return redirect("/");
|
||||||
|
const isAdmin = checkAccess(user, ["developer", "admin"]);
|
||||||
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
|
const entities = await (isAdmin
|
||||||
|
? getEntitiesWithRoles()
|
||||||
|
: getEntitiesWithRoles(entityIDS));
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const allowedEntities = findAllowedEntities(
|
||||||
const entities = await (checkAccess(user, ["developer", "admin"]) ? getEntitiesWithRoles() : getEntitiesWithRoles(entityIDS));
|
user,
|
||||||
|
entities,
|
||||||
|
"view_assignments"
|
||||||
|
);
|
||||||
|
const [users, assignments] = await Promise.all([
|
||||||
|
await (isAdmin
|
||||||
|
? getUsers({}, 0, {}, { _id: 0, id: 1, name: 1 })
|
||||||
|
: getEntitiesUsers(mapBy(allowedEntities, "id"), {}, 0, {
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
})),
|
||||||
|
await (isAdmin
|
||||||
|
? getAssignments()
|
||||||
|
: getEntitiesAssignments(mapBy(allowedEntities, "id"))),
|
||||||
|
]);
|
||||||
|
|
||||||
const allowedEntities = findAllowedEntities(user, entities, "view_assignments")
|
return {
|
||||||
|
props: serialize({ user, users, entities: allowedEntities, assignments }),
|
||||||
const users =
|
};
|
||||||
await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
|
|
||||||
|
|
||||||
const assignments =
|
|
||||||
await (checkAccess(user, ["developer", "admin"]) ? getAssignments() : getEntitiesAssignments(mapBy(allowedEntities, 'id')));
|
|
||||||
|
|
||||||
return { props: serialize({ user, users, entities: allowedEntities, assignments }) };
|
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const SEARCH_FIELDS = [["name"]];
|
const SEARCH_FIELDS = [["name"]];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
entities: EntityWithRoles[]
|
entities: EntityWithRoles[];
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssignmentsPage({ assignments, entities, user, users }: Props) {
|
export default function AssignmentsPage({
|
||||||
const entitiesAllowCreate = useAllowedEntities(user, entities, 'create_assignment')
|
assignments,
|
||||||
const entitiesAllowEdit = useAllowedEntities(user, entities, 'edit_assignment')
|
entities,
|
||||||
const entitiesAllowArchive = useAllowedEntities(user, entities, 'archive_assignment')
|
user,
|
||||||
|
users,
|
||||||
|
}: Props) {
|
||||||
|
const entitiesAllowCreate = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"create_assignment"
|
||||||
|
);
|
||||||
|
const entitiesAllowEdit = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"edit_assignment"
|
||||||
|
);
|
||||||
|
const entitiesAllowArchive = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"archive_assignment"
|
||||||
|
);
|
||||||
|
|
||||||
const activeAssignments = useMemo(() => assignments.filter(activeAssignmentFilter), [assignments]);
|
const activeAssignments = useMemo(
|
||||||
const plannedAssignments = useMemo(() => assignments.filter(futureAssignmentFilter), [assignments]);
|
() => assignments.filter(activeAssignmentFilter),
|
||||||
const pastAssignments = useMemo(() => assignments.filter(pastAssignmentFilter), [assignments]);
|
[assignments]
|
||||||
const startExpiredAssignments = useMemo(() => assignments.filter(startHasExpiredAssignmentFilter), [assignments]);
|
);
|
||||||
const archivedAssignments = useMemo(() => assignments.filter(archivedAssignmentFilter), [assignments]);
|
const plannedAssignments = useMemo(
|
||||||
|
() => assignments.filter(futureAssignmentFilter),
|
||||||
|
[assignments]
|
||||||
|
);
|
||||||
|
const pastAssignments = useMemo(
|
||||||
|
() => assignments.filter(pastAssignmentFilter),
|
||||||
|
[assignments]
|
||||||
|
);
|
||||||
|
const startExpiredAssignments = useMemo(
|
||||||
|
() => assignments.filter(startHasExpiredAssignmentFilter),
|
||||||
|
[assignments]
|
||||||
|
);
|
||||||
|
const archivedAssignments = useMemo(
|
||||||
|
() => assignments.filter(archivedAssignmentFilter),
|
||||||
|
[assignments]
|
||||||
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { rows: activeRows, renderSearch: renderActive } = useListSearch(SEARCH_FIELDS, activeAssignments);
|
const { rows: activeRows, renderSearch: renderActive } = useListSearch(
|
||||||
const { rows: plannedRows, renderSearch: renderPlanned } = useListSearch(SEARCH_FIELDS, plannedAssignments);
|
SEARCH_FIELDS,
|
||||||
const { rows: pastRows, renderSearch: renderPast } = useListSearch(SEARCH_FIELDS, pastAssignments);
|
activeAssignments
|
||||||
const { rows: expiredRows, renderSearch: renderExpired } = useListSearch(SEARCH_FIELDS, startExpiredAssignments);
|
);
|
||||||
const { rows: archivedRows, renderSearch: renderArchived } = useListSearch(SEARCH_FIELDS, archivedAssignments);
|
const { rows: plannedRows, renderSearch: renderPlanned } = useListSearch(
|
||||||
|
SEARCH_FIELDS,
|
||||||
|
plannedAssignments
|
||||||
|
);
|
||||||
|
const { rows: pastRows, renderSearch: renderPast } = useListSearch(
|
||||||
|
SEARCH_FIELDS,
|
||||||
|
pastAssignments
|
||||||
|
);
|
||||||
|
const { rows: expiredRows, renderSearch: renderExpired } = useListSearch(
|
||||||
|
SEARCH_FIELDS,
|
||||||
|
startExpiredAssignments
|
||||||
|
);
|
||||||
|
const { rows: archivedRows, renderSearch: renderArchived } = useListSearch(
|
||||||
|
SEARCH_FIELDS,
|
||||||
|
archivedAssignments
|
||||||
|
);
|
||||||
|
|
||||||
const { items: activeItems, renderMinimal: paginationActive } = usePagination(activeRows, 16);
|
const { items: activeItems, renderMinimal: paginationActive } = usePagination(
|
||||||
const { items: plannedItems, renderMinimal: paginationPlanned } = usePagination(plannedRows, 16);
|
activeRows,
|
||||||
const { items: pastItems, renderMinimal: paginationPast } = usePagination(pastRows, 16);
|
16
|
||||||
const { items: expiredItems, renderMinimal: paginationExpired } = usePagination(expiredRows, 16);
|
);
|
||||||
const { items: archivedItems, renderMinimal: paginationArchived } = usePagination(archivedRows, 16);
|
const { items: plannedItems, renderMinimal: paginationPlanned } =
|
||||||
|
usePagination(plannedRows, 16);
|
||||||
|
const { items: pastItems, renderMinimal: paginationPast } = usePagination(
|
||||||
|
pastRows,
|
||||||
|
16
|
||||||
|
);
|
||||||
|
const { items: expiredItems, renderMinimal: paginationExpired } =
|
||||||
|
usePagination(expiredRows, 16);
|
||||||
|
const { items: archivedItems, renderMinimal: paginationArchived } =
|
||||||
|
usePagination(archivedRows, 16);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Assignments | EnCoach</title>
|
<title>Assignments | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/dashboard" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link
|
||||||
<BsChevronLeft />
|
href="/dashboard"
|
||||||
</Link>
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
<h2 className="font-bold text-2xl">Assignments</h2>
|
>
|
||||||
</div>
|
<BsChevronLeft />
|
||||||
<Separator />
|
</Link>
|
||||||
</div>
|
<h2 className="font-bold text-2xl">Assignments</h2>
|
||||||
<div className="flex flex-col gap-2">
|
</div>
|
||||||
<span className="text-lg font-bold">Active Assignments Status</span>
|
<Separator />
|
||||||
<div className="flex items-center gap-4">
|
</div>
|
||||||
<span>
|
<div className="flex flex-col gap-2">
|
||||||
<b>Total:</b> {activeAssignments.reduce((acc, curr) => acc + curr.results.length, 0)}/
|
<span className="text-lg font-bold">Active Assignments Status</span>
|
||||||
{activeAssignments.reduce((acc, curr) => curr.exams.length + acc, 0)}
|
<div className="flex items-center gap-4">
|
||||||
</span>
|
<span>
|
||||||
</div>
|
<b>Total:</b>{" "}
|
||||||
</div>
|
{activeAssignments.reduce(
|
||||||
|
(acc, curr) => acc + curr.results.length,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
/
|
||||||
|
{activeAssignments.reduce(
|
||||||
|
(acc, curr) => curr.exams.length + acc,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Active Assignments ({activeAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
<div className="w-full flex items-center gap-4">
|
Active Assignments ({activeAssignments.length})
|
||||||
{renderActive()}
|
</h2>
|
||||||
{paginationActive()}
|
<div className="w-full flex items-center gap-4">
|
||||||
</div>
|
{renderActive()}
|
||||||
<div className="flex flex-wrap gap-2">
|
{paginationActive()}
|
||||||
{activeItems.map((a) => (
|
</div>
|
||||||
<AssignmentCard {...a} entityObj={findBy(entities, 'id', a.entity)} users={users} onClick={() => router.push(`/assignments/${a.id}`)} key={a.id} />
|
<div className="flex flex-wrap gap-2">
|
||||||
))}
|
{activeItems.map((a) => (
|
||||||
</div>
|
<AssignmentCard
|
||||||
</section>
|
{...a}
|
||||||
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
|
users={users}
|
||||||
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
|
key={a.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Planned Assignments ({plannedAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
<div className="w-full flex items-center gap-4">
|
Planned Assignments ({plannedAssignments.length})
|
||||||
{renderPlanned()}
|
</h2>
|
||||||
{paginationPlanned()}
|
<div className="w-full flex items-center gap-4">
|
||||||
</div>
|
{renderPlanned()}
|
||||||
<div className="flex flex-wrap gap-2">
|
{paginationPlanned()}
|
||||||
<Link
|
</div>
|
||||||
href={entitiesAllowCreate.length > 0 ? "/assignments/creator" : ""}
|
<div className="flex flex-wrap gap-2">
|
||||||
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">
|
<Link
|
||||||
<BsPlus className="text-6xl" />
|
href={
|
||||||
<span className="text-lg">New Assignment</span>
|
entitiesAllowCreate.length > 0 ? "/assignments/creator" : ""
|
||||||
</Link>
|
}
|
||||||
{plannedItems.map((a) => (
|
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"
|
||||||
<AssignmentCard
|
>
|
||||||
{...a}
|
<BsPlus className="text-6xl" />
|
||||||
users={users}
|
<span className="text-lg">New Assignment</span>
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
</Link>
|
||||||
onClick={
|
{plannedItems.map((a) => (
|
||||||
mapBy(entitiesAllowEdit, 'id').includes(a.entity || "")
|
<AssignmentCard
|
||||||
? () => router.push(`/assignments/creator/${a.id}`)
|
{...a}
|
||||||
: () => router.push(`/assignments/${a.id}`)
|
users={users}
|
||||||
}
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
key={a.id}
|
onClick={
|
||||||
/>
|
mapBy(entitiesAllowEdit, "id").includes(a.entity || "")
|
||||||
))}
|
? () => router.push(`/assignments/creator/${a.id}`)
|
||||||
</div>
|
: () => router.push(`/assignments/${a.id}`)
|
||||||
</section>
|
}
|
||||||
|
key={a.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Past Assignments ({pastAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
<div className="w-full flex items-center gap-4">
|
Past Assignments ({pastAssignments.length})
|
||||||
{renderPast()}
|
</h2>
|
||||||
{paginationPast()}
|
<div className="w-full flex items-center gap-4">
|
||||||
</div>
|
{renderPast()}
|
||||||
<div className="flex flex-wrap gap-2">
|
{paginationPast()}
|
||||||
{pastItems.map((a) => (
|
</div>
|
||||||
<AssignmentCard
|
<div className="flex flex-wrap gap-2">
|
||||||
{...a}
|
{pastItems.map((a) => (
|
||||||
users={users}
|
<AssignmentCard
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
{...a}
|
||||||
onClick={() => router.push(`/assignments/${a.id}`)}
|
users={users}
|
||||||
key={a.id}
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
allowDownload
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
allowArchive={mapBy(entitiesAllowArchive, 'id').includes(a.entity || "")}
|
key={a.id}
|
||||||
allowExcelDownload
|
allowDownload
|
||||||
/>
|
allowArchive={mapBy(entitiesAllowArchive, "id").includes(
|
||||||
))}
|
a.entity || ""
|
||||||
</div>
|
)}
|
||||||
</section>
|
allowExcelDownload
|
||||||
<section className="flex flex-col gap-4">
|
/>
|
||||||
<h2 className="text-2xl font-semibold">Assignments start expired ({startExpiredAssignments.length})</h2>
|
))}
|
||||||
<div className="w-full flex items-center gap-4">
|
</div>
|
||||||
{renderExpired()}
|
</section>
|
||||||
{paginationExpired()}
|
<section className="flex flex-col gap-4">
|
||||||
</div>
|
<h2 className="text-2xl font-semibold">
|
||||||
<div className="flex flex-wrap gap-2">
|
Assignments start expired ({startExpiredAssignments.length})
|
||||||
{expiredItems.map((a) => (
|
</h2>
|
||||||
<AssignmentCard
|
<div className="w-full flex items-center gap-4">
|
||||||
{...a}
|
{renderExpired()}
|
||||||
users={users}
|
{paginationExpired()}
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
</div>
|
||||||
onClick={() => router.push(`/assignments/${a.id}`)}
|
<div className="flex flex-wrap gap-2">
|
||||||
key={a.id}
|
{expiredItems.map((a) => (
|
||||||
allowDownload
|
<AssignmentCard
|
||||||
allowArchive={mapBy(entitiesAllowArchive, 'id').includes(a.entity || "")}
|
{...a}
|
||||||
allowExcelDownload
|
users={users}
|
||||||
/>
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
))}
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
</div>
|
key={a.id}
|
||||||
</section>
|
allowDownload
|
||||||
<section className="flex flex-col gap-4">
|
allowArchive={mapBy(entitiesAllowArchive, "id").includes(
|
||||||
<h2 className="text-2xl font-semibold">Archived Assignments ({archivedAssignments.length})</h2>
|
a.entity || ""
|
||||||
<div className="w-full flex items-center gap-4">
|
)}
|
||||||
{renderArchived()}
|
allowExcelDownload
|
||||||
{paginationArchived()}
|
/>
|
||||||
</div>
|
))}
|
||||||
<div className="flex flex-wrap gap-2">
|
</div>
|
||||||
{archivedItems.map((a) => (
|
</section>
|
||||||
<AssignmentCard
|
<section className="flex flex-col gap-4">
|
||||||
{...a}
|
<h2 className="text-2xl font-semibold">
|
||||||
users={users}
|
Archived Assignments ({archivedAssignments.length})
|
||||||
onClick={() => router.push(`/assignments/${a.id}`)}
|
</h2>
|
||||||
key={a.id}
|
<div className="w-full flex items-center gap-4">
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
{renderArchived()}
|
||||||
allowDownload
|
{paginationArchived()}
|
||||||
allowUnarchive
|
</div>
|
||||||
allowExcelDownload
|
<div className="flex flex-wrap gap-2">
|
||||||
/>
|
{archivedItems.map((a) => (
|
||||||
))}
|
<AssignmentCard
|
||||||
</div>
|
{...a}
|
||||||
</section>
|
users={users}
|
||||||
</>
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
</>
|
key={a.id}
|
||||||
);
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
|
allowDownload
|
||||||
|
allowUnarchive
|
||||||
|
allowExcelDownload
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,328 +18,476 @@ import { getEntityUsers, getSpecificUsers } from "@/utils/users.be";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize, last } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Divider } from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { BsBuilding, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsFillPersonVcardFill, BsPlus, BsStopwatchFill, BsTag, BsTrash, BsX } from "react-icons/bs";
|
import {
|
||||||
|
BsBuilding,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsClockFill,
|
||||||
|
BsEnvelopeFill,
|
||||||
|
BsFillPersonVcardFill,
|
||||||
|
BsPlus,
|
||||||
|
BsStopwatchFill,
|
||||||
|
BsTag,
|
||||||
|
BsTrash,
|
||||||
|
BsX,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
|
export const getServerSideProps = withIronSessionSsr(
|
||||||
const user = await requestUser(req, res)
|
async ({ req, res, params }) => {
|
||||||
if (!user) return redirect("/login")
|
const user = await requestUser(req, res);
|
||||||
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const { id } = params as { id: string };
|
const { id } = params as { id: string };
|
||||||
|
|
||||||
const group = await getGroup(id);
|
const group = await getGroup(id);
|
||||||
if (!group || !group.entity) return redirect("/classrooms")
|
if (!group || !group.entity) return redirect("/classrooms");
|
||||||
|
|
||||||
const entity = await getEntityWithRoles(group.entity)
|
const entity = await getEntityWithRoles(group.entity);
|
||||||
if (!entity) return redirect("/classrooms")
|
if (!entity) return redirect("/classrooms");
|
||||||
|
|
||||||
const canView = doesEntityAllow(user, entity, "view_classrooms")
|
const canView = doesEntityAllow(user, entity, "view_classrooms");
|
||||||
if (!canView) return redirect("/")
|
if (!canView) return redirect("/");
|
||||||
|
const [linkedUsers, users] = await Promise.all([
|
||||||
|
getEntityUsers(
|
||||||
|
entity.id,
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
corporateInformation: 1,
|
||||||
|
type: 1,
|
||||||
|
profilePicture: 1,
|
||||||
|
subscriptionExpirationDate: 1,
|
||||||
|
lastLogin: 1,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
getSpecificUsers([...group.participants, group.admin], {
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
corporateInformation: 1,
|
||||||
|
type: 1,
|
||||||
|
profilePicture: 1,
|
||||||
|
subscriptionExpirationDate: 1,
|
||||||
|
lastLogin: 1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const linkedUsers = await getEntityUsers(entity.id)
|
const groupWithUser = convertToUsers(group, users);
|
||||||
const users = await getSpecificUsers([...group.participants, group.admin]);
|
|
||||||
const groupWithUser = convertToUsers(group, users);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, group: groupWithUser, users: linkedUsers.filter(x => isAdmin(user) ? true : !isAdmin(x)), entity }),
|
props: serialize({
|
||||||
};
|
user,
|
||||||
}, sessionOptions);
|
group: groupWithUser,
|
||||||
|
users: linkedUsers.filter((x) => (isAdmin(user) ? true : !isAdmin(x))),
|
||||||
|
entity,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
group: GroupWithUsers;
|
group: GroupWithUsers;
|
||||||
users: User[];
|
users: User[];
|
||||||
entity: EntityWithRoles
|
entity: EntityWithRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ user, group, users, entity }: Props) {
|
export default function Home({ user, group, users, entity }: Props) {
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
|
|
||||||
const canAddParticipants = useEntityPermission(user, entity, "add_to_classroom")
|
const canAddParticipants = useEntityPermission(
|
||||||
const canRemoveParticipants = useEntityPermission(user, entity, "remove_from_classroom")
|
user,
|
||||||
const canRenameClassroom = useEntityPermission(user, entity, "rename_classrooms")
|
entity,
|
||||||
const canDeleteClassroom = useEntityPermission(user, entity, "delete_classroom")
|
"add_to_classroom"
|
||||||
|
);
|
||||||
|
const canRemoveParticipants = useEntityPermission(
|
||||||
|
user,
|
||||||
|
entity,
|
||||||
|
"remove_from_classroom"
|
||||||
|
);
|
||||||
|
const canRenameClassroom = useEntityPermission(
|
||||||
|
user,
|
||||||
|
entity,
|
||||||
|
"rename_classrooms"
|
||||||
|
);
|
||||||
|
const canDeleteClassroom = useEntityPermission(
|
||||||
|
user,
|
||||||
|
entity,
|
||||||
|
"delete_classroom"
|
||||||
|
);
|
||||||
|
|
||||||
const nonParticipantUsers = useMemo(
|
const nonParticipantUsers = useMemo(
|
||||||
() => users.filter((x) => ![...group.participants.map((g) => g.id), group.admin.id, user.id].includes(x.id)),
|
() =>
|
||||||
[users, group.participants, group.admin.id, user.id],
|
users.filter(
|
||||||
);
|
(x) =>
|
||||||
|
![
|
||||||
|
...group.participants.map((g) => g.id),
|
||||||
|
group.admin.id,
|
||||||
|
user.id,
|
||||||
|
].includes(x.id)
|
||||||
|
),
|
||||||
|
[users, group.participants, group.admin.id, user.id]
|
||||||
|
);
|
||||||
|
|
||||||
const { rows, renderSearch } = useListSearch<User>(
|
const { rows, renderSearch } = useListSearch<User>(
|
||||||
[["name"], ["corporateInformation", "companyInformation", "name"]],
|
[["name"], ["corporateInformation", "companyInformation", "name"]],
|
||||||
isAdding ? nonParticipantUsers : group.participants,
|
isAdding ? nonParticipantUsers : group.participants
|
||||||
);
|
);
|
||||||
const { items, renderMinimal } = usePagination<User>(rows, 20);
|
const { items, renderMinimal } = usePagination<User>(rows, 20);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]));
|
const toggleUser = (u: User) =>
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]
|
||||||
|
);
|
||||||
|
|
||||||
const removeParticipants = () => {
|
const removeParticipants = () => {
|
||||||
if (selectedUsers.length === 0) return;
|
if (selectedUsers.length === 0) return;
|
||||||
if (!canRemoveParticipants) return;
|
if (!canRemoveParticipants) return;
|
||||||
if (!confirm(`Are you sure you want to remove ${selectedUsers.length} participant${selectedUsers.length === 1 ? "" : "s"} from this group?`))
|
if (
|
||||||
return;
|
!confirm(
|
||||||
|
`Are you sure you want to remove ${selectedUsers.length} participant${
|
||||||
|
selectedUsers.length === 1 ? "" : "s"
|
||||||
|
} from this group?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/groups/${group.id}`, { participants: group.participants.map((x) => x.id).filter((x) => !selectedUsers.includes(x)) })
|
.patch(`/api/groups/${group.id}`, {
|
||||||
.then(() => {
|
participants: group.participants
|
||||||
toast.success("The group has been updated successfully!");
|
.map((x) => x.id)
|
||||||
router.replace(router.asPath);
|
.filter((x) => !selectedUsers.includes(x)),
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.then(() => {
|
||||||
console.error(e);
|
toast.success("The group has been updated successfully!");
|
||||||
toast.error("Something went wrong!");
|
router.replace(router.asPath);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.catch((e) => {
|
||||||
};
|
console.error(e);
|
||||||
|
toast.error("Something went wrong!");
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
const addParticipants = () => {
|
const addParticipants = () => {
|
||||||
if (selectedUsers.length === 0) return;
|
if (selectedUsers.length === 0) return;
|
||||||
if (!canAddParticipants || !isAdding) return;
|
if (!canAddParticipants || !isAdding) return;
|
||||||
if (!confirm(`Are you sure you want to add ${selectedUsers.length} participant${selectedUsers.length === 1 ? "" : "s"} to this group?`))
|
if (
|
||||||
return;
|
!confirm(
|
||||||
|
`Are you sure you want to add ${selectedUsers.length} participant${
|
||||||
|
selectedUsers.length === 1 ? "" : "s"
|
||||||
|
} to this group?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/groups/${group.id}`, { participants: [...group.participants.map((x) => x.id), ...selectedUsers] })
|
.patch(`/api/groups/${group.id}`, {
|
||||||
.then(() => {
|
participants: [
|
||||||
toast.success("The group has been updated successfully!");
|
...group.participants.map((x) => x.id),
|
||||||
router.replace(router.asPath);
|
...selectedUsers,
|
||||||
})
|
],
|
||||||
.catch((e) => {
|
})
|
||||||
console.error(e);
|
.then(() => {
|
||||||
toast.error("Something went wrong!");
|
toast.success("The group has been updated successfully!");
|
||||||
})
|
router.replace(router.asPath);
|
||||||
.finally(() => setIsLoading(false));
|
})
|
||||||
};
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Something went wrong!");
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
const renameGroup = () => {
|
const renameGroup = () => {
|
||||||
if (!canRenameClassroom) return;
|
if (!canRenameClassroom) return;
|
||||||
|
|
||||||
const name = prompt("Rename this classroom:", group.name);
|
const name = prompt("Rename this classroom:", group.name);
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.patch(`/api/groups/${group.id}`, { name })
|
.patch(`/api/groups/${group.id}`, { name })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The classroom has been updated successfully!");
|
toast.success("The classroom has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Something went wrong!");
|
toast.error("Something went wrong!");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteGroup = () => {
|
const deleteGroup = () => {
|
||||||
if (!canDeleteClassroom) return;
|
if (!canDeleteClassroom) return;
|
||||||
if (!confirm("Are you sure you want to delete this classroom?")) return;
|
if (!confirm("Are you sure you want to delete this classroom?")) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/groups/${group.id}`)
|
.delete(`/api/groups/${group.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("This classroom has been successfully deleted!");
|
toast.success("This classroom has been successfully deleted!");
|
||||||
router.replace("/classrooms");
|
router.replace("/classrooms");
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error("Something went wrong!");
|
toast.error("Something went wrong!");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => setSelectedUsers([]), [isAdding]);
|
useEffect(() => setSelectedUsers([]), [isAdding]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{group.name} | EnCoach</title>
|
<title>{group.name} | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
href="/classrooms"
|
href="/classrooms"
|
||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
<BsChevronLeft />
|
>
|
||||||
</Link>
|
<BsChevronLeft />
|
||||||
<h2 className="font-bold text-2xl">{group.name}</h2>
|
</Link>
|
||||||
</div>
|
<h2 className="font-bold text-2xl">{group.name}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!isAdding && (
|
{!isAdding && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={renameGroup}
|
onClick={renameGroup}
|
||||||
disabled={isLoading || !canRenameClassroom}
|
disabled={isLoading || !canRenameClassroom}
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full hover:bg-neutral-100 disabled:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
className="flex items-center gap-1 px-2 py-2 border rounded-full hover:bg-neutral-100 disabled:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
<BsTag />
|
>
|
||||||
<span className="text-xs">Rename Classroom</span>
|
<BsTag />
|
||||||
</button>
|
<span className="text-xs">Rename Classroom</span>
|
||||||
<button
|
</button>
|
||||||
onClick={deleteGroup}
|
<button
|
||||||
disabled={isLoading || !canDeleteClassroom}
|
onClick={deleteGroup}
|
||||||
className="flex items-center gap-1 px-2 py-2 border border-mti-rose rounded-full bg-mti-rose-light text-white hover:bg-mti-rose-dark disabled:hover:bg-mti-rose-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
disabled={isLoading || !canDeleteClassroom}
|
||||||
<BsTrash />
|
className="flex items-center gap-1 px-2 py-2 border border-mti-rose rounded-full bg-mti-rose-light text-white hover:bg-mti-rose-dark disabled:hover:bg-mti-rose-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
<span className="text-xs">Delete Classroom</span>
|
>
|
||||||
</button>
|
<BsTrash />
|
||||||
</div>
|
<span className="text-xs">Delete Classroom</span>
|
||||||
)}
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
)}
|
||||||
<span className="flex items-center gap-2">
|
</div>
|
||||||
<BsBuilding className="text-xl" /> {entity.label}
|
<div className="flex flex-col gap-2">
|
||||||
</span>
|
<span className="flex items-center gap-2">
|
||||||
<span className="flex items-center gap-2">
|
<BsBuilding className="text-xl" /> {entity.label}
|
||||||
<BsFillPersonVcardFill className="text-xl" /> {getUserName(group.admin)}
|
</span>
|
||||||
</span>
|
<span className="flex items-center gap-2">
|
||||||
</div>
|
<BsFillPersonVcardFill className="text-xl" />{" "}
|
||||||
</div>
|
{getUserName(group.admin)}
|
||||||
<Divider />
|
</span>
|
||||||
<div className="flex items-center justify-between mb-4">
|
</div>
|
||||||
<span className="font-semibold text-xl">Participants</span>
|
</div>
|
||||||
{!isAdding && (
|
<Divider />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<button
|
<span className="font-semibold text-xl">Participants</span>
|
||||||
onClick={() => setIsAdding(true)}
|
{!isAdding && (
|
||||||
disabled={isLoading || !canAddParticipants}
|
<div className="flex items-center gap-2">
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full hover:bg-neutral-100 disabled:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
<button
|
||||||
<BsPlus />
|
onClick={() => setIsAdding(true)}
|
||||||
<span className="text-xs">Add Participants</span>
|
disabled={isLoading || !canAddParticipants}
|
||||||
</button>
|
className="flex items-center gap-1 px-2 py-2 border rounded-full hover:bg-neutral-100 disabled:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
<button
|
>
|
||||||
onClick={removeParticipants}
|
<BsPlus />
|
||||||
disabled={selectedUsers.length === 0 || isLoading || !canRemoveParticipants}
|
<span className="text-xs">Add Participants</span>
|
||||||
className="flex items-center gap-1 px-2 py-2 border border-mti-rose rounded-full bg-mti-rose-light text-white hover:bg-mti-rose-dark disabled:hover:bg-mti-rose-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
</button>
|
||||||
<BsTrash />
|
<button
|
||||||
<span className="text-xs">Remove Participants</span>
|
onClick={removeParticipants}
|
||||||
</button>
|
disabled={
|
||||||
</div>
|
selectedUsers.length === 0 ||
|
||||||
)}
|
isLoading ||
|
||||||
{isAdding && (
|
!canRemoveParticipants
|
||||||
<div className="flex items-center gap-2">
|
}
|
||||||
<button
|
className="flex items-center gap-1 px-2 py-2 border border-mti-rose rounded-full bg-mti-rose-light text-white hover:bg-mti-rose-dark disabled:hover:bg-mti-rose-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
onClick={() => setIsAdding(false)}
|
>
|
||||||
disabled={isLoading}
|
<BsTrash />
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-rose bg-mti-rose-light text-white hover:bg-mti-rose-dark disabled:hover:bg-mti-rose-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
<span className="text-xs">Remove Participants</span>
|
||||||
<BsX />
|
</button>
|
||||||
<span className="text-xs">Discard Selection</span>
|
</div>
|
||||||
</button>
|
)}
|
||||||
<button
|
{isAdding && (
|
||||||
onClick={addParticipants}
|
<div className="flex items-center gap-2">
|
||||||
disabled={selectedUsers.length === 0 || isLoading || !canAddParticipants}
|
<button
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
onClick={() => setIsAdding(false)}
|
||||||
<BsPlus />
|
disabled={isLoading}
|
||||||
<span className="text-xs">Add Participants</span>
|
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-rose bg-mti-rose-light text-white hover:bg-mti-rose-dark disabled:hover:bg-mti-rose-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
</button>
|
>
|
||||||
</div>
|
<BsX />
|
||||||
)}
|
<span className="text-xs">Discard Selection</span>
|
||||||
</div>
|
</button>
|
||||||
<div className="w-full flex items-center gap-4">
|
<button
|
||||||
{renderSearch()}
|
onClick={addParticipants}
|
||||||
{renderMinimal()}
|
disabled={
|
||||||
</div>
|
selectedUsers.length === 0 ||
|
||||||
<div className="flex items-center gap-2 mt-4">
|
isLoading ||
|
||||||
{['student', 'teacher', 'corporate'].map((type) => (
|
!canAddParticipants
|
||||||
<button
|
}
|
||||||
key={type}
|
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
onClick={() => {
|
>
|
||||||
const typeUsers = mapBy(filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type), 'id')
|
<BsPlus />
|
||||||
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
<span className="text-xs">Add Participants</span>
|
||||||
setSelectedUsers((prev) => prev.filter((a) => !typeUsers.includes(a)));
|
</button>
|
||||||
} else {
|
</div>
|
||||||
setSelectedUsers((prev) => [...prev.filter((a) => !typeUsers.includes(a)), ...typeUsers]);
|
)}
|
||||||
}
|
</div>
|
||||||
}}
|
<div className="w-full flex items-center gap-4">
|
||||||
disabled={filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type).length === 0}
|
{renderSearch()}
|
||||||
className={clsx(
|
{renderMinimal()}
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
</div>
|
||||||
"transition duration-300 ease-in-out",
|
<div className="flex items-center gap-2 mt-4">
|
||||||
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
{["student", "teacher", "corporate"].map((type) => (
|
||||||
filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type).length > 0 &&
|
<button
|
||||||
filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type).every((u) => selectedUsers.includes(u.id)) &&
|
key={type}
|
||||||
"!bg-mti-purple-light !text-white",
|
onClick={() => {
|
||||||
)}>
|
const typeUsers = mapBy(
|
||||||
{capitalize(type)}
|
filterBy(
|
||||||
</button>
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
))}
|
"type",
|
||||||
</div>
|
type
|
||||||
</section>
|
),
|
||||||
|
"id"
|
||||||
|
);
|
||||||
|
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.filter((a) => !typeUsers.includes(a))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedUsers((prev) => [
|
||||||
|
...prev.filter((a) => !typeUsers.includes(a)),
|
||||||
|
...typeUsers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
filterBy(
|
||||||
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
|
"type",
|
||||||
|
type
|
||||||
|
).length === 0
|
||||||
|
}
|
||||||
|
className={clsx(
|
||||||
|
"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",
|
||||||
|
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
||||||
|
filterBy(
|
||||||
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
|
"type",
|
||||||
|
type
|
||||||
|
).length > 0 &&
|
||||||
|
filterBy(
|
||||||
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
|
"type",
|
||||||
|
type
|
||||||
|
).every((u) => selectedUsers.includes(u.id)) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{capitalize(type)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<section className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{items.map((u) => (
|
{items.map((u) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleUser(u)}
|
onClick={() => toggleUser(u)}
|
||||||
disabled={isAdding ? !canAddParticipants : !canRemoveParticipants}
|
disabled={
|
||||||
key={u.id}
|
isAdding ? !canAddParticipants : !canRemoveParticipants
|
||||||
className={clsx(
|
}
|
||||||
"p-4 pr-6 h-48 relative border rounded-xl flex flex-col gap-3 justify-between text-left cursor-pointer",
|
key={u.id}
|
||||||
"hover:border-mti-purple transition ease-in-out duration-300",
|
className={clsx(
|
||||||
selectedUsers.includes(u.id) && "border-mti-purple",
|
"p-4 pr-6 h-48 relative border rounded-xl flex flex-col gap-3 justify-between text-left cursor-pointer",
|
||||||
)}>
|
"hover:border-mti-purple transition ease-in-out duration-300",
|
||||||
<div className="flex items-center gap-2">
|
selectedUsers.includes(u.id) && "border-mti-purple"
|
||||||
<div className="min-w-[3rem] min-h-[3rem] w-12 h-12 border flex items-center justify-center overflow-hidden rounded-full">
|
)}
|
||||||
<img src={u.profilePicture} alt={u.name} />
|
>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex flex-col">
|
<div className="min-w-[3rem] min-h-[3rem] w-12 h-12 border flex items-center justify-center overflow-hidden rounded-full">
|
||||||
<span className="font-semibold">{getUserName(u)}</span>
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
<span className="opacity-80 text-sm">{USER_TYPE_LABELS[u.type]}</span>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col">
|
||||||
</div>
|
<span className="font-semibold">{getUserName(u)}</span>
|
||||||
|
<span className="opacity-80 text-sm">
|
||||||
|
{USER_TYPE_LABELS[u.type]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="E-mail address">
|
<Tooltip tooltip="E-mail address">
|
||||||
<BsEnvelopeFill />
|
<BsEnvelopeFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.email}
|
{u.email}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
</span>
|
? moment(u.subscriptionExpirationDate).format(
|
||||||
<span className="flex items-center gap-2">
|
"DD/MM/YYYY"
|
||||||
<Tooltip tooltip="Last Login">
|
)
|
||||||
<BsClockFill />
|
: "Unlimited"}
|
||||||
</Tooltip>
|
</span>
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
<span className="flex items-center gap-2">
|
||||||
</span>
|
<Tooltip tooltip="Last Login">
|
||||||
</div>
|
<BsClockFill />
|
||||||
</button>
|
</Tooltip>
|
||||||
))}
|
{u.lastLogin
|
||||||
</section>
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
</>
|
: "N/A"}
|
||||||
)}
|
</span>
|
||||||
</>
|
</div>
|
||||||
);
|
</button>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,221 +2,308 @@
|
|||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import {EntityWithRoles} from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import {filterBy, mapBy, redirect, serialize} from "@/utils";
|
import { filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntitiesWithRoles} from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import {getUserName, isAdmin} from "@/utils/users";
|
import { getUserName, isAdmin } from "@/utils/users";
|
||||||
import {getEntitiesUsers} from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import {BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill} from "react-icons/bs";
|
import {
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
BsCheck,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsClockFill,
|
||||||
|
BsEnvelopeFill,
|
||||||
|
BsStopwatchFill,
|
||||||
|
} from "react-icons/bs";
|
||||||
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { findAllowedEntities } from "@/utils/permissions";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : mapBy(user.entities, "id"));
|
const entities = await getEntitiesWithRoles(
|
||||||
const users = await getEntitiesUsers(mapBy(entities, 'id'))
|
isAdmin(user) ? undefined : mapBy(user.entities, "id")
|
||||||
const allowedEntities = findAllowedEntities(user, entities, "create_classroom")
|
);
|
||||||
|
const users = await getEntitiesUsers(
|
||||||
|
mapBy(entities, "id"),
|
||||||
|
{
|
||||||
|
id: { $ne: user.id },
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
profilePicture: 1,
|
||||||
|
type: 1,
|
||||||
|
corporateInformation: 1,
|
||||||
|
lastLogin: 1,
|
||||||
|
subscriptionExpirationDate: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const allowedEntities = findAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"create_classroom"
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({user, entities: allowedEntities, users: users.filter((x) => x.id !== user.id)}),
|
props: serialize({
|
||||||
};
|
user,
|
||||||
|
entities: allowedEntities,
|
||||||
|
users: users,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({user, users, entities}: Props) {
|
export default function Home({ user, users, entities }: Props) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [entity, setEntity] = useState<string | undefined>(entities[0]?.id);
|
const [entity, setEntity] = useState<string | undefined>(entities[0]?.id);
|
||||||
|
|
||||||
const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [entity, users])
|
const entityUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
!entity
|
||||||
|
? users
|
||||||
|
: users.filter((u) => mapBy(u.entities, "id").includes(entity)),
|
||||||
|
[entity, users]
|
||||||
|
);
|
||||||
|
|
||||||
const {rows, renderSearch} = useListSearch<User>(
|
const { rows, renderSearch } = useListSearch<User>(
|
||||||
[["name"], ["type"], ["corporateInformation", "companyInformation", "name"]], entityUsers
|
[
|
||||||
);
|
["name"],
|
||||||
|
["type"],
|
||||||
|
["corporateInformation", "companyInformation", "name"],
|
||||||
|
],
|
||||||
|
entityUsers
|
||||||
|
);
|
||||||
|
|
||||||
const {items, renderMinimal} = usePagination<User>(rows, 16);
|
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => setSelectedUsers([]), [entity])
|
useEffect(() => setSelectedUsers([]), [entity]);
|
||||||
|
|
||||||
const createGroup = () => {
|
const createGroup = () => {
|
||||||
if (!name.trim()) return;
|
if (!name.trim()) return;
|
||||||
if (!entity) return;
|
if (!entity) return;
|
||||||
if (!confirm(`Are you sure you want to create this group with ${selectedUsers.length} participants?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to create this group with ${selectedUsers.length} participants?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<{id: string}>(`/api/groups`, {name, participants: selectedUsers, admin: user.id, entity})
|
.post<{ id: string }>(`/api/groups`, {
|
||||||
.then((result) => {
|
name,
|
||||||
toast.success("Your group has been created successfully!");
|
participants: selectedUsers,
|
||||||
router.replace(`/classrooms/${result.data.id}`);
|
admin: user.id,
|
||||||
})
|
entity,
|
||||||
.catch((e) => {
|
})
|
||||||
console.error(e);
|
.then((result) => {
|
||||||
toast.error("Something went wrong!");
|
toast.success("Your group has been created successfully!");
|
||||||
})
|
router.replace(`/classrooms/${result.data.id}`);
|
||||||
.finally(() => setIsLoading(false));
|
})
|
||||||
};
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast.error("Something went wrong!");
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]));
|
const toggleUser = (u: User) =>
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Create Group | EnCoach</title>
|
<title>Create Group | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex gap-3 justify-between">
|
<div className="flex gap-3 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
href="/classrooms"
|
href="/classrooms"
|
||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
<BsChevronLeft />
|
>
|
||||||
</Link>
|
<BsChevronLeft />
|
||||||
<h2 className="font-bold text-2xl">Create Classroom</h2>
|
</Link>
|
||||||
</div>
|
<h2 className="font-bold text-2xl">Create Classroom</h2>
|
||||||
<div className="flex items-center gap-4">
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-4">
|
||||||
onClick={createGroup}
|
<button
|
||||||
disabled={!name.trim() || !entity || isLoading}
|
onClick={createGroup}
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
disabled={!name.trim() || !entity || isLoading}
|
||||||
<BsCheck />
|
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
<span className="text-xs">Create Classroom</span>
|
>
|
||||||
</button>
|
<BsCheck />
|
||||||
</div>
|
<span className="text-xs">Create Classroom</span>
|
||||||
</div>
|
</button>
|
||||||
<Divider />
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4 place-items-end">
|
</div>
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<Divider />
|
||||||
<span className="font-semibold text-xl">Classroom Name:</span>
|
<div className="grid grid-cols-2 gap-4 place-items-end">
|
||||||
<Input name="name" onChange={setName} type="text" placeholder="Classroom A" />
|
<div className="flex flex-col gap-4 w-full">
|
||||||
</div>
|
<span className="font-semibold text-xl">Classroom Name:</span>
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<Input
|
||||||
<span className="font-semibold text-xl">Entity:</span>
|
name="name"
|
||||||
<Select
|
onChange={setName}
|
||||||
options={entities.map((e) => ({value: e.id, label: e.label}))}
|
type="text"
|
||||||
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
placeholder="Classroom A"
|
||||||
defaultValue={{value: entities[0]?.id, label: entities[0]?.label}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col gap-4 w-full">
|
||||||
</div>
|
<span className="font-semibold text-xl">Entity:</span>
|
||||||
<Divider />
|
<Select
|
||||||
<div className="flex items-center justify-between mb-4">
|
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||||
<span className="font-semibold text-xl">Participants ({selectedUsers.length} selected):</span>
|
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
||||||
</div>
|
defaultValue={{
|
||||||
<div className="w-full flex items-center gap-4">
|
value: entities[0]?.id,
|
||||||
{renderSearch()}
|
label: entities[0]?.label,
|
||||||
{renderMinimal()}
|
}}
|
||||||
</div>
|
/>
|
||||||
<div className="flex items-center gap-2 mt-4">
|
</div>
|
||||||
{['student', 'teacher', 'corporate'].map((type) => (
|
</div>
|
||||||
<button
|
<Divider />
|
||||||
key={type}
|
<div className="flex items-center justify-between mb-4">
|
||||||
onClick={() => {
|
<span className="font-semibold text-xl">
|
||||||
const typeUsers = mapBy(filterBy(entityUsers, 'type', type), 'id')
|
Participants ({selectedUsers.length} selected):
|
||||||
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
</span>
|
||||||
setSelectedUsers((prev) => prev.filter((a) => !typeUsers.includes(a)));
|
</div>
|
||||||
} else {
|
<div className="w-full flex items-center gap-4">
|
||||||
setSelectedUsers((prev) => [...prev.filter((a) => !typeUsers.includes(a)), ...typeUsers]);
|
{renderSearch()}
|
||||||
}
|
{renderMinimal()}
|
||||||
}}
|
</div>
|
||||||
disabled={filterBy(entityUsers, 'type', type).length === 0}
|
<div className="flex items-center gap-2 mt-4">
|
||||||
className={clsx(
|
{["student", "teacher", "corporate"].map((type) => (
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
<button
|
||||||
"transition duration-300 ease-in-out",
|
key={type}
|
||||||
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
onClick={() => {
|
||||||
filterBy(entityUsers, 'type', type).length > 0 &&
|
const typeUsers = mapBy(
|
||||||
filterBy(entityUsers, 'type', type).every((u) => selectedUsers.includes(u.id)) &&
|
filterBy(entityUsers, "type", type),
|
||||||
"!bg-mti-purple-light !text-white",
|
"id"
|
||||||
)}>
|
);
|
||||||
{capitalize(type)}
|
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
||||||
</button>
|
setSelectedUsers((prev) =>
|
||||||
))}
|
prev.filter((a) => !typeUsers.includes(a))
|
||||||
</div>
|
);
|
||||||
</section>
|
} else {
|
||||||
|
setSelectedUsers((prev) => [
|
||||||
|
...prev.filter((a) => !typeUsers.includes(a)),
|
||||||
|
...typeUsers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={filterBy(entityUsers, "type", type).length === 0}
|
||||||
|
className={clsx(
|
||||||
|
"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",
|
||||||
|
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
||||||
|
filterBy(entityUsers, "type", type).length > 0 &&
|
||||||
|
filterBy(entityUsers, "type", type).every((u) =>
|
||||||
|
selectedUsers.includes(u.id)
|
||||||
|
) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{capitalize(type)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<section className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{items.map((u) => (
|
{items.map((u) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleUser(u)}
|
onClick={() => toggleUser(u)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
key={u.id}
|
key={u.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 pr-6 h-48 relative border rounded-xl flex flex-col gap-3 justify-between text-left cursor-pointer",
|
"p-4 pr-6 h-48 relative border rounded-xl flex flex-col gap-3 justify-between text-left cursor-pointer",
|
||||||
"hover:border-mti-purple transition ease-in-out duration-300",
|
"hover:border-mti-purple transition ease-in-out duration-300",
|
||||||
selectedUsers.includes(u.id) && "border-mti-purple",
|
selectedUsers.includes(u.id) && "border-mti-purple"
|
||||||
)}>
|
)}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<div className="min-w-[3rem] min-h-[3rem] w-12 h-12 border flex items-center justify-center overflow-hidden rounded-full">
|
<div className="flex items-center gap-2">
|
||||||
<img src={u.profilePicture} alt={u.name} />
|
<div className="min-w-[3rem] min-h-[3rem] w-12 h-12 border flex items-center justify-center overflow-hidden rounded-full">
|
||||||
</div>
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
<div className="flex flex-col">
|
</div>
|
||||||
<span className="font-semibold">{getUserName(u)}</span>
|
<div className="flex flex-col">
|
||||||
<span className="opacity-80 text-sm">{USER_TYPE_LABELS[u.type]}</span>
|
<span className="font-semibold">{getUserName(u)}</span>
|
||||||
</div>
|
<span className="opacity-80 text-sm">
|
||||||
</div>
|
{USER_TYPE_LABELS[u.type]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="E-mail address">
|
<Tooltip tooltip="E-mail address">
|
||||||
<BsEnvelopeFill />
|
<BsEnvelopeFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.email}
|
{u.email}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
</span>
|
? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY")
|
||||||
<span className="flex items-center gap-2">
|
: "Unlimited"}
|
||||||
<Tooltip tooltip="Last Login">
|
</span>
|
||||||
<BsClockFill />
|
<span className="flex items-center gap-2">
|
||||||
</Tooltip>
|
<Tooltip tooltip="Last Login">
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
<BsClockFill />
|
||||||
</span>
|
</Tooltip>
|
||||||
</div>
|
{u.lastLogin
|
||||||
</button>
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
))}
|
: "N/A"}
|
||||||
</section>
|
</span>
|
||||||
</>
|
</div>
|
||||||
</>
|
</button>
|
||||||
);
|
))}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,132 +27,182 @@ import StudentClassroomTransfer from "@/components/Imports/StudentClassroomTrans
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id");
|
const entityIDS = mapBy(user.entities, "id");
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS)
|
const entities = await getEntitiesWithRoles(
|
||||||
const allowedEntities = findAllowedEntities(user, entities, "view_classrooms")
|
isAdmin(user) ? undefined : entityIDS
|
||||||
|
);
|
||||||
|
|
||||||
const groups = await getGroupsForEntities(mapBy(allowedEntities, 'id'));
|
const allowedEntities = findAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"view_classrooms"
|
||||||
|
);
|
||||||
|
|
||||||
const users = await getSpecificUsers(uniq(groups.flatMap((g) => [...g.participants, g.admin])));
|
const groups = await getGroupsForEntities(mapBy(allowedEntities, "id"));
|
||||||
const groupsWithUsers: GroupWithUsers[] = groups.map((g) => convertToUsers(g, users.filter(x => isAdmin(user) ? true : !isAdmin(x))));
|
|
||||||
|
|
||||||
return {
|
const users = await getSpecificUsers(
|
||||||
props: serialize({ user, groups: groupsWithUsers, entities: allowedEntities }),
|
uniq(groups.flatMap((g) => [...g.participants, g.admin])),
|
||||||
};
|
{ _id: 0, id: 1, name: 1, email: 1, corporateInformation: 1, type: 1 }
|
||||||
|
);
|
||||||
|
const groupsWithUsers: GroupWithUsers[] = groups.map((g) =>
|
||||||
|
convertToUsers(
|
||||||
|
g,
|
||||||
|
users.filter((x) => (isAdmin(user) ? true : !isAdmin(x)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
groups: groupsWithUsers,
|
||||||
|
entities: allowedEntities,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const SEARCH_FIELDS = [
|
const SEARCH_FIELDS = [
|
||||||
["name"],
|
["name"],
|
||||||
["admin", "name"],
|
["admin", "name"],
|
||||||
["admin", "email"],
|
["admin", "email"],
|
||||||
["admin", "corporateInformation", "companyInformation", "name"],
|
["admin", "corporateInformation", "companyInformation", "name"],
|
||||||
["participants", "name"],
|
["participants", "name"],
|
||||||
["participants", "email"],
|
["participants", "email"],
|
||||||
["participants", "corporateInformation", "companyInformation", "name"],
|
["participants", "corporateInformation", "companyInformation", "name"],
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
groups: GroupWithUsers[];
|
groups: GroupWithUsers[];
|
||||||
entities: EntityWithRoles[]
|
entities: EntityWithRoles[];
|
||||||
}
|
}
|
||||||
export default function Home({ user, groups, entities }: Props) {
|
export default function Home({ user, groups, entities }: Props) {
|
||||||
const entitiesAllowCreate = useAllowedEntities(user, entities, 'create_classroom');
|
const entitiesAllowCreate = useAllowedEntities(
|
||||||
const [showImport, setShowImport] = useState(false);
|
user,
|
||||||
|
entities,
|
||||||
|
"create_classroom"
|
||||||
|
);
|
||||||
|
const [showImport, setShowImport] = useState(false);
|
||||||
|
|
||||||
const renderCard = (group: GroupWithUsers) => (
|
const renderCard = (group: GroupWithUsers) => (
|
||||||
<Link
|
<Link
|
||||||
href={`/classrooms/${group.id}`}
|
href={`/classrooms/${group.id}`}
|
||||||
key={group.id}
|
key={group.id}
|
||||||
className="p-4 border-2 border-mti-purple-light/20 rounded-xl flex gap-2 justify-between hover:border-mti-purple group transition ease-in-out duration-300 text-left cursor-pointer">
|
className="p-4 border-2 border-mti-purple-light/20 rounded-xl flex gap-2 justify-between hover:border-mti-purple group transition ease-in-out duration-300 text-left cursor-pointer"
|
||||||
<div className="flex flex-col gap-2 w-full">
|
>
|
||||||
<span className="flex items-center gap-1">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Classroom</span>
|
<span className="flex items-center gap-1">
|
||||||
{group.name}
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
</span>
|
Classroom
|
||||||
<span className="flex items-center gap-1">
|
</span>
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Admin</span>
|
{group.name}
|
||||||
{getUserName(group.admin)}
|
</span>
|
||||||
</span>
|
<span className="flex items-center gap-1">
|
||||||
{!!group.entity && (
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
<span className="flex items-center gap-1">
|
Admin
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Entity</span>
|
</span>
|
||||||
{findBy(entities, 'id', group.entity)?.label}
|
{getUserName(group.admin)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
{!!group.entity && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Participants</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
<span className="bg-mti-purple-light/50 px-2">{group.participants.length}</span>
|
Entity
|
||||||
</span>
|
</span>
|
||||||
<span>
|
{findBy(entities, "id", group.entity)?.label}
|
||||||
{group.participants.slice(0, 3).map(getUserName).join(", ")}{' '}
|
</span>
|
||||||
{group.participants.length > 3 ? <span className="opacity-50 bg-mti-purple-light/50 px-1 text-sm">and {group.participants.length - 3} more</span> : ""}
|
)}
|
||||||
</span>
|
<span className="flex items-center gap-1">
|
||||||
</div>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
<div className="w-fit">
|
Participants
|
||||||
<FaPersonChalkboard className="w-full h-20 -translate-y-[15%] group-hover:text-mti-purple transition ease-in-out duration-300" />
|
</span>
|
||||||
</div>
|
<span className="bg-mti-purple-light/50 px-2">
|
||||||
</Link>
|
{group.participants.length}
|
||||||
);
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{group.participants.slice(0, 3).map(getUserName).join(", ")}{" "}
|
||||||
|
{group.participants.length > 3 ? (
|
||||||
|
<span className="opacity-50 bg-mti-purple-light/50 px-1 text-sm">
|
||||||
|
and {group.participants.length - 3} more
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-fit">
|
||||||
|
<FaPersonChalkboard className="w-full h-20 -translate-y-[15%] group-hover:text-mti-purple transition ease-in-out duration-300" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
const firstCard = () => (
|
const firstCard = () => (
|
||||||
<Link
|
<Link
|
||||||
href={`/classrooms/create`}
|
href={`/classrooms/create`}
|
||||||
className="p-4 border-2 hover:text-mti-purple rounded-xl flex flex-col items-center justify-center gap-0 hover:border-mti-purple transition ease-in-out duration-300 text-left cursor-pointer">
|
className="p-4 border-2 hover:text-mti-purple rounded-xl flex flex-col items-center justify-center gap-0 hover:border-mti-purple transition ease-in-out duration-300 text-left cursor-pointer"
|
||||||
<BsPlus size={40} />
|
>
|
||||||
<span className="font-semibold">Create Classroom</span>
|
<BsPlus size={40} />
|
||||||
</Link>
|
<span className="font-semibold">Create Classroom</span>
|
||||||
);
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Classrooms | EnCoach</title>
|
<title>Classrooms | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-col gap-4 w-full h-full">
|
<section className="flex flex-col gap-4 w-full h-full">
|
||||||
<Modal isOpen={showImport} onClose={() => setShowImport(false)} maxWidth="max-w-[85%]">
|
<Modal
|
||||||
<StudentClassroomTransfer user={user} entities={entities} onFinish={() => setShowImport(false)} />
|
isOpen={showImport}
|
||||||
</Modal>
|
onClose={() => setShowImport(false)}
|
||||||
<div className="flex flex-col gap-4">
|
maxWidth="max-w-[85%]"
|
||||||
<div className="flex justify-between">
|
>
|
||||||
<h2 className="font-bold text-2xl">Classrooms</h2>
|
<StudentClassroomTransfer
|
||||||
{entitiesAllowCreate.length !== 0 && <button
|
user={user}
|
||||||
className={clsx(
|
entities={entities}
|
||||||
"flex flex-row gap-3 items-center py-1.5 px-4 text-lg",
|
onFinish={() => setShowImport(false)}
|
||||||
"bg-mti-purple-light border border-mti-purple-light rounded-xl text-white",
|
/>
|
||||||
"hover:bg-white hover:text-mti-purple-light transition duration-300 ease-in-out",
|
</Modal>
|
||||||
)}
|
<div className="flex flex-col gap-4">
|
||||||
onClick={() => setShowImport(true)}
|
<div className="flex justify-between">
|
||||||
>
|
<h2 className="font-bold text-2xl">Classrooms</h2>
|
||||||
<FaFileUpload className="w-5 h-5" />
|
{entitiesAllowCreate.length !== 0 && (
|
||||||
Transfer Students
|
<button
|
||||||
</button>
|
className={clsx(
|
||||||
}
|
"flex flex-row gap-3 items-center py-1.5 px-4 text-lg",
|
||||||
</div>
|
"bg-mti-purple-light border border-mti-purple-light rounded-xl text-white",
|
||||||
<Separator />
|
"hover:bg-white hover:text-mti-purple-light transition duration-300 ease-in-out"
|
||||||
</div>
|
)}
|
||||||
|
onClick={() => setShowImport(true)}
|
||||||
|
>
|
||||||
|
<FaFileUpload className="w-5 h-5" />
|
||||||
|
Transfer Students
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
|
||||||
<CardList<GroupWithUsers>
|
<CardList<GroupWithUsers>
|
||||||
list={groups}
|
list={groups}
|
||||||
searchFields={SEARCH_FIELDS}
|
searchFields={SEARCH_FIELDS}
|
||||||
renderCard={renderCard}
|
renderCard={renderCard}
|
||||||
firstCard={entitiesAllowCreate.length === 0 ? undefined : firstCard}
|
firstCard={entitiesAllowCreate.length === 0 ? undefined : firstCard}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,15 @@ import IconCard from "@/components/IconCard";
|
|||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Stat, Type, User } from "@/interfaces/user";
|
import { Stat, Type, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { filterBy, mapBy, redirect, serialize } from "@/utils";
|
import { filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import {
|
import { countEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
countEntitiesAssignments,
|
|
||||||
} from "@/utils/assignments.be";
|
|
||||||
import { getEntities } from "@/utils/entities.be";
|
import { getEntities } from "@/utils/entities.be";
|
||||||
import { countGroups } from "@/utils/groups.be";
|
import { countGroups } from "@/utils/groups.be";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
import { getStatsByUsers } from "@/utils/stats.be";
|
||||||
import {
|
import { countUsersByTypes, getUsers } from "@/utils/users.be";
|
||||||
countUsersByTypes,
|
|
||||||
getUsers,
|
|
||||||
} from "@/utils/users.be";
|
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -49,49 +44,48 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
if (!user || !user.isVerified) return redirect("/login");
|
if (!user || !user.isVerified) return redirect("/login");
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer"])) return redirect("/");
|
if (!checkAccess(user, ["admin", "developer"])) return redirect("/");
|
||||||
|
const [
|
||||||
const students = await getUsers(
|
entities,
|
||||||
{ type: "student" },
|
usersCount,
|
||||||
10,
|
groupsCount,
|
||||||
{
|
students,
|
||||||
averageLevel: -1,
|
latestStudents,
|
||||||
},
|
latestTeachers,
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
] = await Promise.all([
|
||||||
);
|
getEntities(undefined, { _id: 0, id: 1, label: 1 }),
|
||||||
|
countUsersByTypes(["student", "teacher", "corporate", "mastercorporate"]),
|
||||||
const usersCount = await countUsersByTypes([
|
countGroups(),
|
||||||
"student",
|
getUsers(
|
||||||
"teacher",
|
{ type: "student" },
|
||||||
"corporate",
|
10,
|
||||||
"mastercorporate",
|
{
|
||||||
|
averageLevel: -1,
|
||||||
|
},
|
||||||
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
),
|
||||||
|
getUsers(
|
||||||
|
{ type: "student" },
|
||||||
|
10,
|
||||||
|
{
|
||||||
|
registrationDate: -1,
|
||||||
|
},
|
||||||
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
),
|
||||||
|
getUsers(
|
||||||
|
{ type: "teacher" },
|
||||||
|
10,
|
||||||
|
{
|
||||||
|
registrationDate: -1,
|
||||||
|
},
|
||||||
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const latestStudents = await getUsers(
|
|
||||||
{ type: "student" },
|
|
||||||
10,
|
|
||||||
{
|
|
||||||
registrationDate: -1,
|
|
||||||
},
|
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
|
||||||
);
|
|
||||||
const latestTeachers = await getUsers(
|
|
||||||
{ type: "teacher" },
|
|
||||||
10,
|
|
||||||
{
|
|
||||||
registrationDate: -1,
|
|
||||||
},
|
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const entities = await getEntities(undefined, { _id: 0, id: 1, label: 1 });
|
|
||||||
|
|
||||||
const assignmentsCount = await countEntitiesAssignments(
|
const assignmentsCount = await countEntitiesAssignments(
|
||||||
mapBy(entities, "id"),
|
mapBy(entities, "id"),
|
||||||
{ archived: { $ne: true } }
|
{ archived: { $ne: true } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupsCount = await countGroups();
|
|
||||||
|
|
||||||
const stats = await getStatsByUsers(mapBy(students, "id"));
|
const stats = await getStatsByUsers(mapBy(students, "id"));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import {
|
|||||||
groupAllowedEntitiesByPermissions,
|
groupAllowedEntitiesByPermissions,
|
||||||
} from "@/utils/permissions";
|
} from "@/utils/permissions";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import {
|
import { countAllowedUsers, getUsers } from "@/utils/users.be";
|
||||||
countAllowedUsers,
|
|
||||||
getUsers,
|
|
||||||
} from "@/utils/users.be";
|
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
@@ -35,6 +32,7 @@ import {
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
|
import { count } from "console";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -68,40 +66,44 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
"view_teachers",
|
"view_teachers",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const allowedStudentEntitiesIDS = mapBy(allowedStudentEntities, "id");
|
const allowedStudentEntitiesIDS = mapBy(allowedStudentEntities, "id");
|
||||||
const entitiesIDS = mapBy(entities, "id") || [];
|
const entitiesIDS = mapBy(entities, "id") || [];
|
||||||
|
|
||||||
|
const [
|
||||||
const students = await getUsers(
|
students,
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
latestStudents,
|
||||||
10,
|
latestTeachers,
|
||||||
{ averageLevel: -1 },
|
userCounts,
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
assignmentsCount,
|
||||||
);
|
groupsCount,
|
||||||
const latestStudents = await getUsers(
|
] = await Promise.all([
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
getUsers(
|
||||||
10,
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
{ registrationDate: -1 },
|
10,
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ averageLevel: -1 },
|
||||||
);
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
const latestTeachers = await getUsers(
|
),
|
||||||
{
|
getUsers(
|
||||||
type: "teacher",
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
10,
|
||||||
},
|
{ registrationDate: -1 },
|
||||||
10,
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
{ registrationDate: -1 },
|
),
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
getUsers(
|
||||||
);
|
{
|
||||||
|
type: "teacher",
|
||||||
const userCounts = await countAllowedUsers(user, entities);
|
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
||||||
|
},
|
||||||
const assignmentsCount = await countEntitiesAssignments(
|
10,
|
||||||
entitiesIDS,
|
{ registrationDate: -1 },
|
||||||
{ archived: { $ne: true } }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
|
countAllowedUsers(user, entities),
|
||||||
const groupsCount = await countGroupsByEntities(entitiesIDS);
|
countEntitiesAssignments(entitiesIDS, {
|
||||||
|
archived: { $ne: true },
|
||||||
|
}),
|
||||||
|
countGroupsByEntities(entitiesIDS),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
@@ -135,7 +137,7 @@ export default function Dashboard({
|
|||||||
userCounts.student +
|
userCounts.student +
|
||||||
userCounts.teacher,
|
userCounts.teacher,
|
||||||
[userCounts]
|
[userCounts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalLicenses = useMemo(
|
const totalLicenses = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -2,21 +2,16 @@
|
|||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Stat, Type, User } from "@/interfaces/user";
|
import { Stat, Type, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { filterBy, mapBy, redirect, serialize } from "@/utils";
|
import { filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import {
|
import { countEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
countEntitiesAssignments,
|
|
||||||
} from "@/utils/assignments.be";
|
|
||||||
import { getEntities } from "@/utils/entities.be";
|
import { getEntities } from "@/utils/entities.be";
|
||||||
import { countGroups } from "@/utils/groups.be";
|
import { countGroups } from "@/utils/groups.be";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import {
|
import { countUsersByTypes, getUsers } from "@/utils/users.be";
|
||||||
countUsersByTypes,
|
|
||||||
getUsers,
|
|
||||||
} from "@/utils/users.be";
|
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -49,45 +44,41 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer"])) return redirect("/");
|
if (!checkAccess(user, ["admin", "developer"])) return redirect("/");
|
||||||
|
|
||||||
const students = await getUsers(
|
const [
|
||||||
{ type: "student" },
|
students,
|
||||||
10,
|
latestStudents,
|
||||||
{
|
latestTeachers,
|
||||||
averageLevel: -1,
|
usersCount,
|
||||||
},
|
entities,
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
groupsCount,
|
||||||
);
|
] = await Promise.all([
|
||||||
|
getUsers(
|
||||||
const usersCount = await countUsersByTypes([
|
{ type: "student" },
|
||||||
"student",
|
10,
|
||||||
"teacher",
|
{ averageLevel: -1 },
|
||||||
"corporate",
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
"mastercorporate",
|
),
|
||||||
|
getUsers(
|
||||||
|
{ type: "student" },
|
||||||
|
10,
|
||||||
|
{ registrationDate: -1 },
|
||||||
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
),
|
||||||
|
getUsers(
|
||||||
|
{ type: "teacher" },
|
||||||
|
10,
|
||||||
|
{ registrationDate: -1 },
|
||||||
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
),
|
||||||
|
countUsersByTypes(["student", "teacher", "corporate", "mastercorporate"]),
|
||||||
|
getEntities(undefined, { _id: 0, id: 1, label: 1 }),
|
||||||
|
countGroups(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const latestStudents = await getUsers(
|
|
||||||
{ type: "student" },
|
|
||||||
10,
|
|
||||||
{
|
|
||||||
registrationDate: -1,
|
|
||||||
},
|
|
||||||
{id:1, name: 1, email: 1, profilePicture: 1 }
|
|
||||||
);
|
|
||||||
const latestTeachers = await getUsers(
|
|
||||||
{ type: "teacher" },
|
|
||||||
10,
|
|
||||||
{
|
|
||||||
registrationDate: -1,
|
|
||||||
},
|
|
||||||
{ id:1,name: 1, email: 1, profilePicture: 1 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const entities = await getEntities(undefined, { _id: 0, id: 1, label: 1 });
|
|
||||||
const assignmentsCount = await countEntitiesAssignments(
|
const assignmentsCount = await countEntitiesAssignments(
|
||||||
mapBy(entities, "id"),
|
mapBy(entities, "id"),
|
||||||
{ archived: { $ne: true } }
|
{ archived: { $ne: true } }
|
||||||
);
|
);
|
||||||
const groupsCount = await countGroups();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
|
import { count } from "console";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -70,37 +71,39 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
|
|
||||||
const entitiesIDS = mapBy(entities, "id") || [];
|
const entitiesIDS = mapBy(entities, "id") || [];
|
||||||
|
|
||||||
const students = await getUsers(
|
const [
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
students,
|
||||||
10,
|
latestStudents,
|
||||||
{ averageLevel: -1 },
|
latestTeachers,
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
userCounts,
|
||||||
);
|
assignmentsCount,
|
||||||
|
groupsCount,
|
||||||
const latestStudents = await getUsers(
|
] = await Promise.all([
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
getUsers(
|
||||||
10,
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
{ registrationDate: -1 },
|
10,
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ averageLevel: -1 },
|
||||||
);
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
),
|
||||||
const latestTeachers = await getUsers(
|
getUsers(
|
||||||
{
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
type: "teacher",
|
10,
|
||||||
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
{ registrationDate: -1 },
|
||||||
},
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
10,
|
),
|
||||||
{ registrationDate: -1 },
|
getUsers(
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{
|
||||||
);
|
type: "teacher",
|
||||||
|
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
||||||
const userCounts = await countAllowedUsers(user, entities);
|
},
|
||||||
|
10,
|
||||||
const assignmentsCount = await countEntitiesAssignments(entitiesIDS, {
|
{ registrationDate: -1 },
|
||||||
archived: { $ne: true },
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
});
|
),
|
||||||
|
countAllowedUsers(user, entities),
|
||||||
const groupsCount = await countGroupsByEntities(entitiesIDS);
|
countEntitiesAssignments(entitiesIDS, { archived: { $ne: true } }),
|
||||||
|
countGroupsByEntities(entitiesIDS),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
@@ -127,6 +130,7 @@ export default function Dashboard({
|
|||||||
stats = [],
|
stats = [],
|
||||||
groupsCount,
|
groupsCount,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
const totalCount = useMemo(
|
const totalCount = useMemo(
|
||||||
() =>
|
() =>
|
||||||
userCounts.corporate +
|
userCounts.corporate +
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import InviteWithUserCard from "@/components/Medium/InviteWithUserCard";
|
|||||||
import ModuleBadge from "@/components/ModuleBadge";
|
import ModuleBadge from "@/components/ModuleBadge";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import { Session } from "@/hooks/useSessions";
|
import { Session } from "@/hooks/useSessions";
|
||||||
import { Grading } from "@/interfaces";
|
import { Grading, Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import { Exam } from "@/interfaces/exam";
|
||||||
import { InviteWithEntity } from "@/interfaces/invite";
|
import { InviteWithEntity } from "@/interfaces/invite";
|
||||||
@@ -34,6 +34,7 @@ import { capitalize, uniqBy } from "lodash";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
BsBook,
|
BsBook,
|
||||||
BsClipboard,
|
BsClipboard,
|
||||||
@@ -65,42 +66,49 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
return redirect("/");
|
return redirect("/");
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
|
|
||||||
const entities = await getEntities(entityIDS, { _id: 0, label: 1 });
|
|
||||||
const currentDate = moment().toISOString();
|
const currentDate = moment().toISOString();
|
||||||
const assignments = await getAssignmentsForStudent(user.id, currentDate);
|
|
||||||
const stats = await getDetailedStatsByUser(user.id, "stats");
|
|
||||||
|
|
||||||
|
const [assignments, stats, invites, grading] = await Promise.all([
|
||||||
|
getAssignmentsForStudent(user.id, currentDate),
|
||||||
|
getDetailedStatsByUser(user.id, "stats"),
|
||||||
|
getInvitesByInvitee(user.id),
|
||||||
|
getGradingSystemByEntity(entityIDS[0] || "", {
|
||||||
|
_id: 0,
|
||||||
|
steps: 1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
const assignmentsIDs = mapBy(assignments, "id");
|
const assignmentsIDs = mapBy(assignments, "id");
|
||||||
|
|
||||||
const sessions = await getSessionsByUser(user.id, 10, {
|
const sessions = await getSessionsByUser(user.id, 10, {
|
||||||
["assignment.id"]: { $in: assignmentsIDs },
|
["assignment.id"]: { $in: assignmentsIDs },
|
||||||
});
|
});
|
||||||
const invites = await getInvitesByInvitee(user.id);
|
|
||||||
const grading = await getGradingSystemByEntity(entityIDS[0] || "", {
|
|
||||||
_id: 0,
|
|
||||||
steps: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedInvites = await Promise.all(
|
const formattedInvites = await Promise.all(
|
||||||
invites.map(convertInvitersToEntity)
|
invites.map(convertInvitersToEntity)
|
||||||
);
|
);
|
||||||
|
|
||||||
const examIDs = uniqBy(
|
const examIDs = uniqBy(
|
||||||
assignments.flatMap((a) =>
|
assignments.reduce<{ module: Module; id: string; key: string }[]>(
|
||||||
a.exams.map((e: { module: string; id: string }) => ({
|
(acc, a) => {
|
||||||
module: e.module,
|
a.exams.forEach((e: { module: Module; id: string }) => {
|
||||||
id: e.id,
|
acc.push({
|
||||||
key: `${e.module}_${e.id}`,
|
module: e.module,
|
||||||
}))
|
id: e.id,
|
||||||
|
key: `${e.module}_${e.id}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
),
|
),
|
||||||
"key"
|
"key"
|
||||||
);
|
);
|
||||||
|
|
||||||
const exams = examIDs.length > 0 ? await getExamsByIds(examIDs) : [];
|
const exams = examIDs.length > 0 ? await getExamsByIds(examIDs) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
user,
|
user,
|
||||||
entities,
|
|
||||||
assignments,
|
assignments,
|
||||||
stats,
|
stats,
|
||||||
exams,
|
exams,
|
||||||
@@ -145,6 +153,11 @@ export default function Dashboard({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const entitiesLabels = useMemo(
|
||||||
|
() => (entities.length > 0 ? mapBy(entities, "label")?.join(", ") : ""),
|
||||||
|
[entities]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -160,7 +173,7 @@ export default function Dashboard({
|
|||||||
<>
|
<>
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="rounded-lg bg-neutral-200 px-2 py-1 ">
|
<div className="rounded-lg bg-neutral-200 px-2 py-1 ">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{entitiesLabels}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { requestUser } from "@/utils/api";
|
|||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -52,29 +53,29 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
|
|
||||||
const filteredEntities = findAllowedEntities(user, entities, "view_students");
|
const filteredEntities = findAllowedEntities(user, entities, "view_students");
|
||||||
|
|
||||||
const students = await getEntitiesUsers(
|
const [students, assignments, groups] = await Promise.all([
|
||||||
mapBy(filteredEntities, "id"),
|
getEntitiesUsers(
|
||||||
{
|
mapBy(filteredEntities, "id"),
|
||||||
type: "student",
|
{
|
||||||
},
|
type: "student",
|
||||||
0,
|
},
|
||||||
{
|
0,
|
||||||
_id: 0,
|
{
|
||||||
id: 1,
|
_id: 0,
|
||||||
name: 1,
|
id: 1,
|
||||||
email: 1,
|
name: 1,
|
||||||
profilePicture: 1,
|
email: 1,
|
||||||
levels: 1,
|
profilePicture: 1,
|
||||||
registrationDate: 1,
|
levels: 1,
|
||||||
}
|
registrationDate: 1,
|
||||||
);
|
}
|
||||||
|
),
|
||||||
const assignments = await getEntitiesAssignments(entityIDS);
|
getEntitiesAssignments(entityIDS),
|
||||||
|
getGroupsByEntities(entityIDS),
|
||||||
|
]);
|
||||||
|
|
||||||
const stats = await getStatsByUsers(students.map((u) => u.id));
|
const stats = await getStatsByUsers(students.map((u) => u.id));
|
||||||
|
|
||||||
const groups = await getGroupsByEntities(entityIDS);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, students, entities, assignments, stats, groups }),
|
props: serialize({ user, students, entities, assignments, stats, groups }),
|
||||||
};
|
};
|
||||||
@@ -100,6 +101,10 @@ export default function Dashboard({
|
|||||||
entities,
|
entities,
|
||||||
"view_student_performance"
|
"view_student_performance"
|
||||||
);
|
);
|
||||||
|
const entitiesLabels = useMemo(
|
||||||
|
() => mapBy(entities, "label")?.join(", "),
|
||||||
|
[entities]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -117,7 +122,7 @@ export default function Dashboard({
|
|||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{entitiesLabels}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
<section className="grid grid-cols-5 -md:grid-cols-2 place-items-center gap-4 text-center">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ import { USER_TYPE_LABELS } from "@/resources/user";
|
|||||||
import { redirect, serialize } from "@/utils";
|
import { redirect, serialize } from "@/utils";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { getUserName } from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
import { getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
@@ -20,161 +20,218 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Divider } from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill } from "react-icons/bs";
|
import {
|
||||||
|
BsCheck,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsClockFill,
|
||||||
|
BsEnvelopeFill,
|
||||||
|
BsStopwatchFill,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
if (!["admin", "developer"].includes(user.type)) return redirect("/entities")
|
if (!["admin", "developer"].includes(user.type)) return redirect("/entities");
|
||||||
|
|
||||||
const users = await getUsers()
|
const users = await getUsers(
|
||||||
|
{ id: { $ne: user.id } },
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
type: 1,
|
||||||
|
profilePicture: 1,
|
||||||
|
email: 1,
|
||||||
|
lastLogin: 1,
|
||||||
|
subscriptionExpirationDate: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, users: users.filter((x) => x.id !== user.id) }),
|
props: serialize({ user, users }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({ user, users }: Props) {
|
export default function Home({ user, users }: Props) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
const [licenses, setLicenses] = useState(0);
|
const [licenses, setLicenses] = useState(0);
|
||||||
|
|
||||||
const { rows, renderSearch } = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], users);
|
const { rows, renderSearch } = useListSearch<User>(
|
||||||
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
[["name"], ["corporateInformation", "companyInformation", "name"]],
|
||||||
|
users
|
||||||
|
);
|
||||||
|
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const createGroup = () => {
|
const createGroup = () => {
|
||||||
if (!label.trim()) return;
|
if (!label.trim()) return;
|
||||||
if (!confirm(`Are you sure you want to create this entity with ${selectedUsers.length} members?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to create this entity with ${selectedUsers.length} members?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post<Entity>(`/api/entities`, { label, licenses, members: selectedUsers })
|
.post<Entity>(`/api/entities`, {
|
||||||
.then((result) => {
|
label,
|
||||||
toast.success("Your entity has been created successfully!");
|
licenses,
|
||||||
router.replace(`/entities/${result.data.id}`);
|
members: selectedUsers,
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.then((result) => {
|
||||||
console.error(e);
|
toast.success("Your entity has been created successfully!");
|
||||||
toast.error("Something went wrong!");
|
router.replace(`/entities/${result.data.id}`);
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.catch((e) => {
|
||||||
};
|
console.error(e);
|
||||||
|
toast.error("Something went wrong!");
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]));
|
const toggleUser = (u: User) =>
|
||||||
|
setSelectedUsers((prev) =>
|
||||||
|
prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Create Entity | EnCoach</title>
|
<title>Create Entity | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex gap-3 justify-between">
|
<div className="flex gap-3 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
href="/classrooms"
|
href="/classrooms"
|
||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
<BsChevronLeft />
|
>
|
||||||
</Link>
|
<BsChevronLeft />
|
||||||
<h2 className="font-bold text-2xl">Create Entity</h2>
|
</Link>
|
||||||
</div>
|
<h2 className="font-bold text-2xl">Create Entity</h2>
|
||||||
<div className="flex items-center gap-4">
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-4">
|
||||||
onClick={createGroup}
|
<button
|
||||||
disabled={!label.trim() || licenses <= 0 || isLoading}
|
onClick={createGroup}
|
||||||
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300">
|
disabled={!label.trim() || licenses <= 0 || isLoading}
|
||||||
<BsCheck />
|
className="flex items-center gap-1 px-2 py-2 border rounded-full border-mti-green bg-mti-green-light text-white hover:bg-mti-green-dark disabled:hover:bg-mti-green-light disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition ease-in-out duration-300"
|
||||||
<span className="text-xs">Create Entity</span>
|
>
|
||||||
</button>
|
<BsCheck />
|
||||||
</div>
|
<span className="text-xs">Create Entity</span>
|
||||||
</div>
|
</button>
|
||||||
<Divider />
|
</div>
|
||||||
<div className="w-full grid grid-cols-2 gap-4">
|
</div>
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<Divider />
|
||||||
<span className="font-semibold text-xl">Entity Label:</span>
|
<div className="w-full grid grid-cols-2 gap-4">
|
||||||
<Input name="name" onChange={setLabel} type="text" placeholder="Entity A" />
|
<div className="flex flex-col gap-4 w-full">
|
||||||
</div>
|
<span className="font-semibold text-xl">Entity Label:</span>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
onChange={setLabel}
|
||||||
|
type="text"
|
||||||
|
placeholder="Entity A"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<span className="font-semibold text-xl">Licenses:</span>
|
<span className="font-semibold text-xl">Licenses:</span>
|
||||||
<Input name="licenses" min={0} onChange={(v) => setLicenses(parseInt(v))} type="number" placeholder="12" />
|
<Input
|
||||||
</div>
|
name="licenses"
|
||||||
</div>
|
min={0}
|
||||||
<Divider />
|
onChange={(v) => setLicenses(parseInt(v))}
|
||||||
<div className="flex items-center justify-between mb-4">
|
type="number"
|
||||||
<span className="font-semibold text-xl">Members ({selectedUsers.length} selected):</span>
|
placeholder="12"
|
||||||
</div>
|
/>
|
||||||
<div className="w-full flex items-center gap-4">
|
</div>
|
||||||
{renderSearch()}
|
</div>
|
||||||
{renderMinimal()}
|
<Divider />
|
||||||
</div>
|
<div className="flex items-center justify-between mb-4">
|
||||||
</section>
|
<span className="font-semibold text-xl">
|
||||||
|
Members ({selectedUsers.length} selected):
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
|
{renderSearch()}
|
||||||
|
{renderMinimal()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<section className="w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{items.map((u) => (
|
{items.map((u) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleUser(u)}
|
onClick={() => toggleUser(u)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
key={u.id}
|
key={u.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 pr-6 h-48 relative border rounded-xl flex flex-col gap-3 justify-between text-left cursor-pointer",
|
"p-4 pr-6 h-48 relative border rounded-xl flex flex-col gap-3 justify-between text-left cursor-pointer",
|
||||||
"hover:border-mti-purple transition ease-in-out duration-300",
|
"hover:border-mti-purple transition ease-in-out duration-300",
|
||||||
selectedUsers.includes(u.id) && "border-mti-purple",
|
selectedUsers.includes(u.id) && "border-mti-purple"
|
||||||
)}>
|
)}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<div className="min-w-[3rem] min-h-[3rem] w-12 h-12 border flex items-center justify-center overflow-hidden rounded-full">
|
<div className="flex items-center gap-2">
|
||||||
<img src={u.profilePicture} alt={u.name} />
|
<div className="min-w-[3rem] min-h-[3rem] w-12 h-12 border flex items-center justify-center overflow-hidden rounded-full">
|
||||||
</div>
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
<div className="flex flex-col">
|
</div>
|
||||||
<span className="font-semibold">{getUserName(u)}</span>
|
<div className="flex flex-col">
|
||||||
<span className="opacity-80 text-sm">{USER_TYPE_LABELS[u.type]}</span>
|
<span className="font-semibold">{getUserName(u)}</span>
|
||||||
</div>
|
<span className="opacity-80 text-sm">
|
||||||
</div>
|
{USER_TYPE_LABELS[u.type]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="E-mail address">
|
<Tooltip tooltip="E-mail address">
|
||||||
<BsEnvelopeFill />
|
<BsEnvelopeFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.email}
|
{u.email}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
</span>
|
? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY")
|
||||||
<span className="flex items-center gap-2">
|
: "Unlimited"}
|
||||||
<Tooltip tooltip="Last Login">
|
</span>
|
||||||
<BsClockFill />
|
<span className="flex items-center gap-2">
|
||||||
</Tooltip>
|
<Tooltip tooltip="Last Login">
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
<BsClockFill />
|
||||||
</span>
|
</Tooltip>
|
||||||
</div>
|
{u.lastLogin
|
||||||
</button>
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
))}
|
: "N/A"}
|
||||||
</section>
|
</span>
|
||||||
</>
|
</div>
|
||||||
</>
|
</button>
|
||||||
);
|
))}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,35 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
);
|
);
|
||||||
const allowedEntities = findAllowedEntities(user, entities, "view_entities");
|
const allowedEntities = findAllowedEntities(user, entities, "view_entities");
|
||||||
|
|
||||||
const entitiesWithCount = await Promise.all(
|
const [counts, users] = await Promise.all([
|
||||||
allowedEntities.map(async (e) => ({
|
await Promise.all(
|
||||||
entity: e,
|
allowedEntities.map(async (e) =>
|
||||||
count: await countEntityUsers(e.id, {
|
countEntityUsers(e.id, {
|
||||||
type: { $in: ["student", "teacher", "corporate", "mastercorporate"] },
|
type: { $in: ["student", "teacher", "corporate", "mastercorporate"] },
|
||||||
}),
|
})
|
||||||
users: await getEntityUsers(e.id, 5, {
|
)
|
||||||
type: { $in: ["student", "teacher", "corporate", "mastercorporate"] },
|
),
|
||||||
}),
|
await Promise.all(
|
||||||
}))
|
allowedEntities.map(async (e) =>
|
||||||
);
|
getEntityUsers(
|
||||||
|
e.id,
|
||||||
|
5,
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
$in: ["student", "teacher", "corporate", "mastercorporate"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 1 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const entitiesWithCount = allowedEntities.map<{
|
||||||
|
entity: EntityWithRoles;
|
||||||
|
users: User[];
|
||||||
|
count: number;
|
||||||
|
}>((e, i) => ({ entity: e, users: users[i], count: counts[i] }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, entities: entitiesWithCount }),
|
props: serialize({ user, entities: entitiesWithCount }),
|
||||||
|
|||||||
@@ -21,93 +21,124 @@ import { getSessionByAssignment } from "@/utils/sessions.be";
|
|||||||
import { Session } from "@/hooks/useSessions";
|
import { Session } from "@/hooks/useSessions";
|
||||||
import { activeAssignmentFilter } from "@/utils/assignments";
|
import { activeAssignmentFilter } from "@/utils/assignments";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
|
export const getServerSideProps = withIronSessionSsr(
|
||||||
const user = await requestUser(req, res)
|
async ({ req, res, query }) => {
|
||||||
const loginDestination = Buffer.from(req.url || "/").toString("base64")
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect(`/login?destination=${loginDestination}`)
|
const loginDestination = Buffer.from(req.url || "/").toString("base64");
|
||||||
|
if (!user) return redirect(`/login?destination=${loginDestination}`);
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const { assignment: assignmentID, destination } = query as { assignment?: string, destination?: string }
|
const { assignment: assignmentID, destination } = query as {
|
||||||
const destinationURL = !!destination ? Buffer.from(destination, 'base64').toString() : undefined
|
assignment?: string;
|
||||||
|
destination?: string;
|
||||||
|
};
|
||||||
|
const destinationURL = !!destination
|
||||||
|
? Buffer.from(destination, "base64").toString()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!!assignmentID) {
|
if (!!assignmentID) {
|
||||||
const assignment = await getAssignment(assignmentID)
|
const assignment = await getAssignment(assignmentID);
|
||||||
|
|
||||||
if (!assignment) return redirect(destinationURL || "/exam")
|
if (!assignment) return redirect(destinationURL || "/exam");
|
||||||
if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type))
|
if (
|
||||||
return redirect(destinationURL || "/exam")
|
!assignment.assignees.includes(user.id) &&
|
||||||
|
!["admin", "developer"].includes(user.type)
|
||||||
|
)
|
||||||
|
return redirect(destinationURL || "/exam");
|
||||||
|
|
||||||
if (filterBy(assignment.results, 'user', user.id).length > 0)
|
if (filterBy(assignment.results, "user", user.id).length > 0)
|
||||||
return redirect(destinationURL || "/exam")
|
return redirect(destinationURL || "/exam");
|
||||||
|
|
||||||
const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
|
const [exams, session] = await Promise.all([
|
||||||
const session = await getSessionByAssignment(assignmentID)
|
getExamsByIds(uniqBy(assignment.exams, "id")),
|
||||||
|
getSessionByAssignment(assignmentID),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, assignment, exams, destinationURL, session: session ?? undefined })
|
props: serialize({
|
||||||
}
|
user,
|
||||||
}
|
assignment,
|
||||||
|
exams,
|
||||||
|
destinationURL,
|
||||||
|
session: session ?? undefined,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, destinationURL }),
|
props: serialize({ user, destinationURL }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
},
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
assignment?: Assignment
|
assignment?: Assignment;
|
||||||
exams?: Exam[]
|
exams?: Exam[];
|
||||||
session?: Session
|
session?: Session;
|
||||||
destinationURL?: string
|
destinationURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page: React.FC<Props> = ({ user, assignment, exams = [], destinationURL = "/exam", session }) => {
|
const Page: React.FC<Props> = ({
|
||||||
const router = useRouter()
|
user,
|
||||||
const { assignment: storeAssignment, dispatch } = useExamStore();
|
assignment,
|
||||||
|
exams = [],
|
||||||
|
destinationURL = "/exam",
|
||||||
|
session,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { assignment: storeAssignment, dispatch } = useExamStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assignment && exams.length > 0 && !storeAssignment && !session) {
|
if (assignment && exams.length > 0 && !storeAssignment && !session) {
|
||||||
if (!activeAssignmentFilter(assignment)) return
|
if (!activeAssignmentFilter(assignment)) return;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "INIT_EXAM", payload: {
|
type: "INIT_EXAM",
|
||||||
exams: exams.sort(sortByModule),
|
payload: {
|
||||||
modules: exams
|
exams: exams.sort(sortByModule),
|
||||||
.map((x) => x!)
|
modules: exams
|
||||||
.sort(sortByModule)
|
.map((x) => x!)
|
||||||
.map((x) => x!.module),
|
.sort(sortByModule)
|
||||||
assignment
|
.map((x) => x!.module),
|
||||||
}
|
assignment,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
router.replace(router.asPath)
|
router.replace(router.asPath);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [assignment, exams, session])
|
}, [assignment, exams, session]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assignment && exams.length > 0 && !storeAssignment && !!session) {
|
if (assignment && exams.length > 0 && !storeAssignment && !!session) {
|
||||||
dispatch({ type: "SET_SESSION", payload: { session } })
|
dispatch({ type: "SET_SESSION", payload: { session } });
|
||||||
router.replace(router.asPath)
|
router.replace(router.asPath);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [assignment, exams, session])
|
}, [assignment, exams, session]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Exams | EnCoach</title>
|
<title>Exams | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ExamPage page="exams" destination={destinationURL} user={user} hideSidebar={!!assignment || !!storeAssignment} />
|
<ExamPage
|
||||||
</>
|
page="exams"
|
||||||
);
|
destination={destinationURL}
|
||||||
}
|
user={user}
|
||||||
|
hideSidebar={!!assignment || !!storeAssignment}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
//Page.whyDidYouRender = true;
|
//Page.whyDidYouRender = true;
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|||||||
@@ -21,92 +21,100 @@ import { getSessionByAssignment } from "@/utils/sessions.be";
|
|||||||
import { Session } from "@/hooks/useSessions";
|
import { Session } from "@/hooks/useSessions";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
|
export const getServerSideProps = withIronSessionSsr(
|
||||||
const user = await requestUser(req, res)
|
async ({ req, res, query }) => {
|
||||||
const destination = Buffer.from(req.url || "/").toString("base64")
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect(`/login?destination=${destination}`)
|
const destination = Buffer.from(req.url || "/").toString("base64");
|
||||||
|
if (!user) return redirect(`/login?destination=${destination}`);
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const { assignment: assignmentID } = query as { assignment?: string }
|
const { assignment: assignmentID } = query as { assignment?: string };
|
||||||
|
|
||||||
if (assignmentID) {
|
if (assignmentID) {
|
||||||
const assignment = await getAssignment(assignmentID)
|
const assignment = await getAssignment(assignmentID);
|
||||||
|
|
||||||
if (!assignment) return redirect("/exam")
|
if (!assignment) return redirect("/exam");
|
||||||
if (!["admin", "developer"].includes(user.type) && !assignment.assignees.includes(user.id)) return redirect("/exercises")
|
if (
|
||||||
|
!["admin", "developer"].includes(user.type) &&
|
||||||
|
!assignment.assignees.includes(user.id)
|
||||||
|
)
|
||||||
|
return redirect("/exercises");
|
||||||
|
if (
|
||||||
|
filterBy(assignment.results, "user", user.id) ||
|
||||||
|
moment(assignment.startDate).isBefore(moment()) ||
|
||||||
|
moment(assignment.endDate).isAfter(moment())
|
||||||
|
)
|
||||||
|
return redirect("/exam");
|
||||||
|
const [exams, session] = await Promise.all([
|
||||||
|
getExamsByIds(uniqBy(assignment.exams, "id")),
|
||||||
|
getSessionByAssignment(assignmentID),
|
||||||
|
]);
|
||||||
|
|
||||||
const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
|
return {
|
||||||
const session = await getSessionByAssignment(assignmentID)
|
props: serialize({ user, assignment, exams, session }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
return {
|
||||||
filterBy(assignment.results, 'user', user.id) ||
|
props: serialize({ user }),
|
||||||
moment(assignment.startDate).isBefore(moment()) ||
|
};
|
||||||
moment(assignment.endDate).isAfter(moment())
|
},
|
||||||
)
|
sessionOptions
|
||||||
return redirect("/exam")
|
);
|
||||||
|
|
||||||
return {
|
|
||||||
props: serialize({ user, assignment, exams, session })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: serialize({ user }),
|
|
||||||
};
|
|
||||||
}, sessionOptions);
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
assignment?: Assignment
|
assignment?: Assignment;
|
||||||
exams?: Exam[]
|
exams?: Exam[];
|
||||||
session?: Session
|
session?: Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page({ user, assignment, exams = [], session }: Props) {
|
export default function Page({ user, assignment, exams = [], session }: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const { assignment: storeAssignment, dispatch } = useExamStore()
|
const { assignment: storeAssignment, dispatch } = useExamStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assignment && exams.length > 0 && !storeAssignment && !session) {
|
if (assignment && exams.length > 0 && !storeAssignment && !session) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "INIT_EXAM", payload: {
|
type: "INIT_EXAM",
|
||||||
exams: exams.sort(sortByModule),
|
payload: {
|
||||||
modules: exams
|
exams: exams.sort(sortByModule),
|
||||||
.map((x) => x!)
|
modules: exams
|
||||||
.sort(sortByModule)
|
.map((x) => x!)
|
||||||
.map((x) => x!.module),
|
.sort(sortByModule)
|
||||||
assignment
|
.map((x) => x!.module),
|
||||||
}
|
assignment,
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
||||||
router.replace(router.asPath)
|
router.replace(router.asPath);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [assignment, exams, session])
|
}, [assignment, exams, session]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assignment && exams.length > 0 && !storeAssignment && !!session) {
|
if (assignment && exams.length > 0 && !storeAssignment && !!session) {
|
||||||
dispatch({ type: "SET_SESSION", payload: { session } });
|
dispatch({ type: "SET_SESSION", payload: { session } });
|
||||||
|
|
||||||
router.replace(router.asPath)
|
router.replace(router.asPath);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [assignment, exams, session])
|
}, [assignment, exams, session]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Exams | EnCoach</title>
|
<title>Exams | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ExamPage page="exams" user={user} hideSidebar={!!assignment} />
|
<ExamPage page="exams" user={user} hideSidebar={!!assignment} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Button from "@/components/Low/Button";
|
|||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import { Session } from "@/hooks/useSessions";
|
import { Session } from "@/hooks/useSessions";
|
||||||
import { Grading } from "@/interfaces";
|
import { Grading, Module } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import { Exam } from "@/interfaces/exam";
|
||||||
import { InviteWithEntity } from "@/interfaces/invite";
|
import { InviteWithEntity } from "@/interfaces/invite";
|
||||||
@@ -12,14 +12,13 @@ import { Assignment } from "@/interfaces/results";
|
|||||||
import { Stat, User } from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
activeAssignmentFilter,
|
activeAssignmentFilter,
|
||||||
futureAssignmentFilter,
|
futureAssignmentFilter,
|
||||||
} from "@/utils/assignments";
|
} from "@/utils/assignments";
|
||||||
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
|
||||||
import { getExamsByIds } from "@/utils/exams.be";
|
import { getExamsByIds } from "@/utils/exams.be";
|
||||||
import { sortByModule } from "@/utils/moduleUtils";
|
import { sortByModule } from "@/utils/moduleUtils";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
@@ -53,32 +52,59 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "student"]))
|
if (!checkAccess(user, ["admin", "developer", "student"]))
|
||||||
return redirect("/");
|
return redirect("/");
|
||||||
|
const assignments = (await getAssignmentsByAssignee(
|
||||||
|
user.id,
|
||||||
|
{
|
||||||
|
archived: { $ne: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
startDate: 1,
|
||||||
|
endDate: 1,
|
||||||
|
exams: 1,
|
||||||
|
results: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: { startDate: 1 },
|
||||||
|
}
|
||||||
|
)) as Assignment[];
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const sessions = await getSessionsByUser(
|
||||||
|
user.id,
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
0,
|
||||||
const assignments = await getAssignmentsByAssignee(user.id, {
|
{
|
||||||
archived: { $ne: true },
|
"assignment.id": { $in: mapBy(assignments, "id") },
|
||||||
});
|
},
|
||||||
const sessions = await getSessionsByUser(user.id, 0, {
|
{
|
||||||
"assignment.id": { $in: mapBy(assignments, "id") },
|
_id: 0,
|
||||||
});
|
id: 1,
|
||||||
|
assignment: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const examIDs = uniqBy(
|
const examIDs = uniqBy(
|
||||||
assignments.flatMap((a) =>
|
assignments.reduce<{ module: Module; id: string; key: string }[]>(
|
||||||
filterBy(a.exams, "assignee", user.id).map(
|
(acc, a) => {
|
||||||
(e: any) => ({
|
a.exams.forEach((e) => {
|
||||||
module: e.module,
|
if (e.assignee === user.id)
|
||||||
id: e.id,
|
acc.push({
|
||||||
key: `${e.module}_${e.id}`,
|
module: e.module,
|
||||||
})
|
id: e.id,
|
||||||
)
|
key: `${e.module}_${e.id}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
),
|
),
|
||||||
"key"
|
"key"
|
||||||
);
|
);
|
||||||
|
|
||||||
const exams = await getExamsByIds(examIDs);
|
const exams = await getExamsByIds(examIDs);
|
||||||
|
|
||||||
return { props: serialize({ user, entities, assignments, exams, sessions }) };
|
return { props: serialize({ user, assignments, exams, sessions }) };
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const destination = Buffer.from("/official-exam").toString("base64");
|
const destination = Buffer.from("/official-exam").toString("base64");
|
||||||
@@ -109,11 +135,12 @@ export default function OfficialExam({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (assignmentExams.every((x) => !!x)) {
|
if (assignmentExams.every((x) => !!x)) {
|
||||||
|
const sortedAssignmentExams = assignmentExams.sort(sortByModule);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "INIT_EXAM",
|
type: "INIT_EXAM",
|
||||||
payload: {
|
payload: {
|
||||||
exams: assignmentExams.sort(sortByModule),
|
exams: sortedAssignmentExams,
|
||||||
modules: mapBy(assignmentExams.sort(sortByModule), "module"),
|
modules: mapBy(sortedAssignmentExams, "module"),
|
||||||
assignment,
|
assignment,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -144,12 +171,16 @@ export default function OfficialExam({
|
|||||||
[assignments]
|
[assignments]
|
||||||
);
|
);
|
||||||
|
|
||||||
const assignmentSessions = useMemo(
|
const assignmentSessions = useMemo(() => {
|
||||||
() =>
|
const studentAssignmentsIDs = mapBy(studentAssignments, "id");
|
||||||
sessions.filter((s) =>
|
return sessions.filter((s) =>
|
||||||
mapBy(studentAssignments, "id").includes(s.assignment?.id || "")
|
studentAssignmentsIDs.includes(s.assignment?.id || "")
|
||||||
),
|
);
|
||||||
[sessions, studentAssignments]
|
}, [sessions, studentAssignments]);
|
||||||
|
|
||||||
|
const entityLabels = useMemo(
|
||||||
|
() => mapBy(entities, "label")?.join(","),
|
||||||
|
[entities]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,7 +198,7 @@ export default function OfficialExam({
|
|||||||
<>
|
<>
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
|
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{entityLabels}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -191,20 +222,18 @@ export default function OfficialExam({
|
|||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
{studentAssignments.length === 0 &&
|
{studentAssignments.length === 0 &&
|
||||||
"Assignments will appear here. It seems that for now there are no assignments for you."}
|
"Assignments will appear here. It seems that for now there are no assignments for you."}
|
||||||
{studentAssignments
|
{studentAssignments.map((a) => (
|
||||||
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
<AssignmentCard
|
||||||
.map((a) => (
|
key={a.id}
|
||||||
<AssignmentCard
|
assignment={a}
|
||||||
key={a.id}
|
user={user}
|
||||||
assignment={a}
|
session={assignmentSessions.find(
|
||||||
user={user}
|
(s) => s.assignment?.id === a.id
|
||||||
session={assignmentSessions.find(
|
)}
|
||||||
(s) => s.assignment?.id === a.id
|
startAssignment={startAssignment}
|
||||||
)}
|
resumeAssignment={loadSession}
|
||||||
startAssignment={startAssignment}
|
/>
|
||||||
resumeAssignment={loadSession}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { useListSearch } from "@/hooks/useListSearch";
|
|||||||
import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, 'id')
|
const entityIDs = mapBy(user.entities, 'id')
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs)
|
|
||||||
|
|
||||||
const domain = user.email.split("@").pop()
|
const domain = user.email.split("@").pop()
|
||||||
const discounts = await db.collection<Discount>("discounts").find({ domain }).toArray()
|
const [entities, discounts, packages] = await Promise.all([
|
||||||
const packages = await db.collection<Package>("packages").find().toArray()
|
getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs),
|
||||||
|
db.collection<Discount>("discounts").find({ domain }).toArray(),
|
||||||
|
db.collection<Package>("packages").find().toArray(),
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, entities, discounts, packages }),
|
props: serialize({ user, entities, discounts, packages }),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
|||||||
import { Permission, PermissionType } from "@/interfaces/permissions";
|
import { Permission, PermissionType } from "@/interfaces/permissions";
|
||||||
import { getPermissionDoc } from "@/utils/permissions.be";
|
import { getPermissionDoc } from "@/utils/permissions.be";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { LayoutContext } from "@/components/High/Layout";
|
import { LayoutContext } from "@/components/High/Layout";
|
||||||
import { getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
@@ -18,6 +18,7 @@ import { Type as UserType } from "@/interfaces/user";
|
|||||||
import { getGroups } from "@/utils/groups.be";
|
import { getGroups } from "@/utils/groups.be";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { redirect } from "@/utils";
|
import { redirect } from "@/utils";
|
||||||
|
import { G } from "@react-pdf/renderer";
|
||||||
interface BasicUser {
|
interface BasicUser {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -40,31 +41,25 @@ export const getServerSideProps = withIronSessionSsr(
|
|||||||
if (!params?.id) return redirect("/permissions");
|
if (!params?.id) return redirect("/permissions");
|
||||||
|
|
||||||
// Fetch data from external API
|
// Fetch data from external API
|
||||||
const permission: Permission = await getPermissionDoc(params.id as string);
|
const [permission, users, groups] = await Promise.all([
|
||||||
|
getPermissionDoc(params.id as string),
|
||||||
const allUserData: User[] = await getUsers();
|
getUsers({}, 0, {}, { _id: 0, id: 1, name: 1, type: 1 }),
|
||||||
const groups = await getGroups();
|
getGroups(),
|
||||||
|
]);
|
||||||
|
|
||||||
const userGroups = groups.filter((x) => x.admin === user.id);
|
const userGroups = groups.filter((x) => x.admin === user.id);
|
||||||
|
const userGroupsParticipants = userGroups.flatMap((x) => x.participants);
|
||||||
const filteredGroups =
|
const filteredGroups =
|
||||||
user.type === "corporate"
|
user.type === "corporate"
|
||||||
? userGroups
|
? userGroups
|
||||||
: user.type === "mastercorporate"
|
: user.type === "mastercorporate"
|
||||||
? groups.filter((x) =>
|
? groups.filter((x) => userGroupsParticipants.includes(x.admin))
|
||||||
userGroups.flatMap((y) => y.participants).includes(x.admin)
|
|
||||||
)
|
|
||||||
: groups;
|
: groups;
|
||||||
|
const filteredGroupsParticipants = filteredGroups.flatMap(
|
||||||
const users = allUserData.map((u) => ({
|
(g) => g.participants
|
||||||
id: u.id,
|
);
|
||||||
name: u.name,
|
|
||||||
type: u.type,
|
|
||||||
})) as BasicUser[];
|
|
||||||
|
|
||||||
const filteredUsers = ["mastercorporate", "corporate"].includes(user.type)
|
const filteredUsers = ["mastercorporate", "corporate"].includes(user.type)
|
||||||
? users.filter((u) =>
|
? users.filter((u) => filteredGroupsParticipants.includes(u.id))
|
||||||
filteredGroups.flatMap((g) => g.participants).includes(u.id)
|
|
||||||
)
|
|
||||||
: users;
|
: users;
|
||||||
|
|
||||||
// const res = await fetch("api/permissions");
|
// const res = await fetch("api/permissions");
|
||||||
@@ -158,12 +153,14 @@ export default function Page(props: Props) {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Select
|
<Select
|
||||||
value={null}
|
value={null}
|
||||||
options={users
|
options={users.reduce<{ label: string; value: string }[]>(
|
||||||
.filter((u) => !selectedUsers.includes(u.id))
|
(acc, u) => {
|
||||||
.map((u) => ({
|
if (!selectedUsers.includes(u.id))
|
||||||
label: `${u?.type}-${u?.name}`,
|
acc.push({ label: `${u?.type}-${u?.name}`, value: u.id });
|
||||||
value: u.id,
|
return acc;
|
||||||
}))}
|
},
|
||||||
|
[]
|
||||||
|
)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<Button onClick={update}>Update</Button>
|
<Button onClick={update}>Update</Button>
|
||||||
@@ -195,9 +192,8 @@ export default function Page(props: Props) {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h2>Whitelisted Users</h2>
|
<h2>Whitelisted Users</h2>
|
||||||
<div className="flex flex-col gap-3 flex-wrap">
|
<div className="flex flex-col gap-3 flex-wrap">
|
||||||
{users
|
{users.map((user) => {
|
||||||
.filter((user) => !selectedUsers.includes(user.id))
|
if (!selectedUsers.includes(user.id))
|
||||||
.map((user) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4"
|
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4"
|
||||||
@@ -208,7 +204,8 @@ export default function Page(props: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
return null;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,23 +53,23 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
if (!user) return redirect("/login");
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/");
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
const linkedCorporate = (await getUserCorporate(user.id)) || null;
|
const [linkedCorporate, groups, referralAgent] = await Promise.all([
|
||||||
const groups = (
|
getUserCorporate(user.id) || null,
|
||||||
await getParticipantGroups(user.id, { _id: 0, admin: 1 })
|
getParticipantGroups(user.id, { _id: 0, group: 1 }),
|
||||||
).map((group) => group.admin);
|
|
||||||
const referralAgent =
|
|
||||||
user.type === "corporate" && user.corporateInformation.referralAgent
|
user.type === "corporate" && user.corporateInformation.referralAgent
|
||||||
? await getUser(user.corporateInformation.referralAgent, {
|
? getUser(user.corporateInformation.referralAgent, {
|
||||||
_id: 0,
|
_id: 0,
|
||||||
name: 1,
|
name: 1,
|
||||||
email: 1,
|
email: 1,
|
||||||
demographicInformation: 1,
|
demographicInformation: 1,
|
||||||
})
|
})
|
||||||
: null;
|
: null,
|
||||||
|
]);
|
||||||
|
const groupsAdmin = groups.map((group) => group.admin);
|
||||||
|
|
||||||
const hasBenefitsFromUniversity =
|
const hasBenefitsFromUniversity =
|
||||||
(await countUsers({
|
(await countUsers({
|
||||||
id: { $in: groups },
|
id: { $in: groupsAdmin },
|
||||||
type: "corporate",
|
type: "corporate",
|
||||||
})) > 0;
|
})) > 0;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
|||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { getGradingSystemByEntities, getGradingSystemByEntity } from "@/utils/grading.be";
|
import {
|
||||||
|
getGradingSystemByEntities,
|
||||||
|
} from "@/utils/grading.be";
|
||||||
import { Grading } from "@/interfaces";
|
import { Grading } from "@/interfaces";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
@@ -33,208 +35,272 @@ import getPendingEvals from "@/utils/disabled.be";
|
|||||||
import useEvaluationPolling from "@/hooks/useEvaluationPolling";
|
import useEvaluationPolling from "@/hooks/useEvaluationPolling";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, 'id')
|
const entityIDs = mapBy(user.entities, "id");
|
||||||
const isAdmin = checkAccess(user, ["admin", "developer"])
|
const isAdmin = checkAccess(user, ["admin", "developer"]);
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs)
|
const entities = await getEntitiesWithRoles(isAdmin ? undefined : entityIDs);
|
||||||
const entitiesIds = mapBy(entities, 'id')
|
const entitiesIds = mapBy(entities, "id");
|
||||||
const users = await (isAdmin ? getUsers() : getEntitiesUsers(entitiesIds))
|
const [users, assignments, gradingSystems, pendingSessionIds] =
|
||||||
const assignments = await (isAdmin ? getAssignments() : getEntitiesAssignments(entitiesIds))
|
await Promise.all([
|
||||||
const gradingSystems = await getGradingSystemByEntities(entitiesIds)
|
isAdmin ? getUsers() : getEntitiesUsers(entitiesIds),
|
||||||
const pendingSessionIds = await getPendingEvals(user.id);
|
isAdmin ? getAssignments() : getEntitiesAssignments(entitiesIds),
|
||||||
|
getGradingSystemByEntities(entitiesIds),
|
||||||
|
getPendingEvals(user.id),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, users, assignments, entities, gradingSystems, isAdmin, pendingSessionIds }),
|
props: serialize({
|
||||||
};
|
user,
|
||||||
|
users,
|
||||||
|
assignments,
|
||||||
|
entities,
|
||||||
|
gradingSystems,
|
||||||
|
isAdmin,
|
||||||
|
pendingSessionIds,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
type Filter = "months" | "weeks" | "days" | "assignments" | undefined;
|
type Filter = "months" | "weeks" | "days" | "assignments" | undefined;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
entities: EntityWithRoles[]
|
entities: EntityWithRoles[];
|
||||||
gradingSystems: Grading[]
|
gradingSystems: Grading[];
|
||||||
pendingSessionIds: string[];
|
pendingSessionIds: string[];
|
||||||
isAdmin:boolean
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TRAINING_EXAMS = 10;
|
const MAX_TRAINING_EXAMS = 10;
|
||||||
|
|
||||||
export default function History({ user, users, assignments, entities, gradingSystems, isAdmin, pendingSessionIds }: Props) {
|
export default function History({
|
||||||
const router = useRouter();
|
user,
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
users,
|
||||||
state.selectedUser,
|
assignments,
|
||||||
state.setSelectedUser,
|
entities,
|
||||||
state.training,
|
gradingSystems,
|
||||||
state.setTraining,
|
isAdmin,
|
||||||
]);
|
pendingSessionIds,
|
||||||
|
}: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore(
|
||||||
|
(state) => [
|
||||||
|
state.selectedUser,
|
||||||
|
state.setSelectedUser,
|
||||||
|
state.training,
|
||||||
|
state.setTraining,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const [filter, setFilter] = useState<Filter>();
|
const [filter, setFilter] = useState<Filter>();
|
||||||
|
|
||||||
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<
|
||||||
const allowedDownloadEntities = useAllowedEntities(user, entities, 'download_student_record')
|
Stat[]
|
||||||
|
>(statsUserId || user?.id);
|
||||||
|
const allowedDownloadEntities = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"download_student_record"
|
||||||
|
);
|
||||||
|
|
||||||
const renderPdfIcon = usePDFDownload("stats");
|
const renderPdfIcon = usePDFDownload("stats");
|
||||||
|
|
||||||
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>(
|
||||||
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
[]
|
||||||
|
);
|
||||||
|
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
||||||
|
|
||||||
const groupedStats = useMemo(() => groupByDate(
|
const groupedStats = useMemo(
|
||||||
stats.filter((x) => {
|
() =>
|
||||||
if (
|
groupByDate(
|
||||||
(
|
stats.filter((x) => {
|
||||||
x.module === "writing" || x.module === "speaking") &&
|
if (
|
||||||
!x.isDisabled && Array.isArray(x.solutions) &&
|
(x.module === "writing" || x.module === "speaking") &&
|
||||||
!x.solutions.every((y) => Object.keys(y).includes("evaluation")
|
!x.isDisabled &&
|
||||||
)
|
Array.isArray(x.solutions) &&
|
||||||
)
|
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
|
||||||
return false;
|
)
|
||||||
return true;
|
return false;
|
||||||
}),
|
return true;
|
||||||
), [stats])
|
})
|
||||||
|
),
|
||||||
|
[stats]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
setTraining(false);
|
setTraining(false);
|
||||||
};
|
};
|
||||||
router.events.on("routeChangeStart", handleRouteChange);
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeStart", handleRouteChange);
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
};
|
};
|
||||||
}, [router.events, setTraining]);
|
}, [router.events, setTraining]);
|
||||||
|
|
||||||
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
|
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
|
||||||
if (filter && filter !== "assignments") {
|
if (filter && filter !== "assignments") {
|
||||||
const filterDate = moment()
|
const filterDate = moment()
|
||||||
.subtract({ [filter as string]: 1 })
|
.subtract({ [filter as string]: 1 })
|
||||||
.format("x");
|
.format("x");
|
||||||
const filteredStats: { [key: string]: Stat[] } = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
if (timestamp >= filterDate)
|
||||||
});
|
filteredStats[timestamp] = stats[timestamp];
|
||||||
return filteredStats;
|
});
|
||||||
}
|
return filteredStats;
|
||||||
|
}
|
||||||
|
|
||||||
if (filter && filter === "assignments") {
|
if (filter && filter === "assignments") {
|
||||||
const filteredStats: { [key: string]: Stat[] } = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
if (
|
||||||
filteredStats[timestamp] = [...stats[timestamp].filter((s) => !!s.assignment)];
|
stats[timestamp]
|
||||||
});
|
.map((s) => s.assignment === undefined)
|
||||||
|
.includes(false)
|
||||||
|
)
|
||||||
|
filteredStats[timestamp] = [
|
||||||
|
...stats[timestamp].filter((s) => !!s.assignment),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
return filteredStats;
|
return filteredStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||||
const allStats = Object.keys(groupedStatsByDate);
|
const allStats = Object.keys(groupedStatsByDate);
|
||||||
const selectedStats = selectedTrainingExams.reduce<Record<string, Stat[]>>((accumulator, moduleAndTimestamp) => {
|
const selectedStats = selectedTrainingExams.reduce<
|
||||||
const timestamp = moduleAndTimestamp.split("-")[1];
|
Record<string, Stat[]>
|
||||||
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
>((accumulator, moduleAndTimestamp) => {
|
||||||
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
const timestamp = moduleAndTimestamp.split("-")[1];
|
||||||
}
|
if (
|
||||||
return accumulator;
|
allStats.includes(timestamp) &&
|
||||||
}, {});
|
!accumulator.hasOwnProperty(timestamp)
|
||||||
setTrainingStats(Object.values(selectedStats).flat());
|
) {
|
||||||
router.push("/training");
|
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
||||||
}
|
}
|
||||||
};
|
return accumulator;
|
||||||
|
}, {});
|
||||||
|
setTrainingStats(Object.values(selectedStats).flat());
|
||||||
|
router.push("/training");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredStats = useMemo(() =>
|
const filteredStats = useMemo(
|
||||||
Object.keys(filterStatsByDate(groupedStats))
|
() =>
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a)),
|
Object.keys(filterStatsByDate(groupedStats)).sort(
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
(a, b) => parseInt(b) - parseInt(a)
|
||||||
[groupedStats, filter])
|
),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[groupedStats, filter]
|
||||||
|
);
|
||||||
|
|
||||||
const customContent = (timestamp: string) => {
|
const customContent = (timestamp: string) => {
|
||||||
const dateStats = groupedStats[timestamp];
|
const dateStats = groupedStats[timestamp];
|
||||||
const statUser = findBy(users, 'id', dateStats[0]?.user)
|
const statUser = findBy(users, "id", dateStats[0]?.user);
|
||||||
|
|
||||||
const canDownload = mapBy(statUser?.entities, 'id').some(e => mapBy(allowedDownloadEntities, 'id').includes(e))
|
const canDownload = mapBy(statUser?.entities, "id").some((e) =>
|
||||||
|
mapBy(allowedDownloadEntities, "id").includes(e)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsGridItem
|
<StatsGridItem
|
||||||
key={uuidv4()}
|
key={uuidv4()}
|
||||||
stats={dateStats}
|
stats={dateStats}
|
||||||
gradingSystems={gradingSystems}
|
gradingSystems={gradingSystems}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
user={user}
|
user={user}
|
||||||
assignments={assignments}
|
assignments={assignments}
|
||||||
users={users}
|
users={users}
|
||||||
training={training}
|
training={training}
|
||||||
selectedTrainingExams={selectedTrainingExams}
|
selectedTrainingExams={selectedTrainingExams}
|
||||||
setSelectedTrainingExams={setSelectedTrainingExams}
|
setSelectedTrainingExams={setSelectedTrainingExams}
|
||||||
maxTrainingExams={MAX_TRAINING_EXAMS}
|
maxTrainingExams={MAX_TRAINING_EXAMS}
|
||||||
renderPdfIcon={canDownload ? renderPdfIcon : undefined}
|
renderPdfIcon={canDownload ? renderPdfIcon : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEvaluationPolling(pendingSessionIds ? pendingSessionIds : [], "records", user.id);
|
useEvaluationPolling(
|
||||||
|
pendingSessionIds ? pendingSessionIds : [],
|
||||||
|
"records",
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Record | EnCoach</title>
|
<title>Record | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<RecordFilter user={user} isAdmin={isAdmin} entities={entities} filterState={{ filter: filter, setFilter: setFilter }}>
|
<RecordFilter
|
||||||
{training && (
|
user={user}
|
||||||
<div className="flex flex-row">
|
isAdmin={isAdmin}
|
||||||
<div className="font-semibold text-2xl mr-4">
|
entities={entities}
|
||||||
Select up to 10 exercises
|
filterState={{ filter: filter, setFilter: setFilter }}
|
||||||
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
|
>
|
||||||
</div>
|
{training && (
|
||||||
<button
|
<div className="flex flex-row">
|
||||||
className={clsx(
|
<div className="font-semibold text-2xl mr-4">
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
|
Select up to 10 exercises
|
||||||
"transition duration-300 ease-in-out",
|
{`(${selectedTrainingExams.length}/${MAX_TRAINING_EXAMS})`}
|
||||||
)}
|
</div>
|
||||||
disabled={selectedTrainingExams.length == 0}
|
<button
|
||||||
onClick={handleTrainingContentSubmission}>
|
className={clsx(
|
||||||
Submit
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
|
||||||
</button>
|
"transition duration-300 ease-in-out"
|
||||||
</div>
|
)}
|
||||||
)}
|
disabled={selectedTrainingExams.length == 0}
|
||||||
</RecordFilter>
|
onClick={handleTrainingContentSubmission}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RecordFilter>
|
||||||
|
|
||||||
|
{filteredStats.length > 0 && !isStatsLoading && (
|
||||||
{filteredStats.length > 0 && !isStatsLoading && (
|
<CardList
|
||||||
<CardList list={filteredStats} renderCard={customContent} searchFields={[]} pageSize={30} className="lg:!grid-cols-3" />
|
list={filteredStats}
|
||||||
)}
|
renderCard={customContent}
|
||||||
{filteredStats.length === 0 && !isStatsLoading && (
|
searchFields={[]}
|
||||||
<span className="font-semibold ml-1">No record to display...</span>
|
pageSize={30}
|
||||||
)}
|
className="lg:!grid-cols-3"
|
||||||
{isStatsLoading && (
|
/>
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
)}
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
{filteredStats.length === 0 && !isStatsLoading && (
|
||||||
</div>
|
<span className="font-semibold ml-1">No record to display...</span>
|
||||||
)}
|
)}
|
||||||
</>
|
{isStatsLoading && (
|
||||||
)}
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||||
</>
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
);
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,169 +13,256 @@ import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill } from "react-icons/bs";
|
import {
|
||||||
|
BsCode,
|
||||||
|
BsCodeSquare,
|
||||||
|
BsGearFill,
|
||||||
|
BsPeopleFill,
|
||||||
|
BsPersonFill,
|
||||||
|
} from "react-icons/bs";
|
||||||
import UserCreator from "./(admin)/UserCreator";
|
import UserCreator from "./(admin)/UserCreator";
|
||||||
import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";
|
import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";
|
||||||
import { CEFR_STEPS } from "@/resources/grading";
|
import { CEFR_STEPS } from "@/resources/grading";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { getUserPermissions } from "@/utils/permissions.be";
|
import { getUserPermissions } from "@/utils/permissions.be";
|
||||||
import { PermissionType } from "@/interfaces/permissions";
|
import { PermissionType } from "@/interfaces/permissions";
|
||||||
import { getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { mapBy, serialize, redirect } from "@/utils";
|
import { mapBy, serialize, redirect } from "@/utils";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
import { getGradingSystemByEntities, getGradingSystemByEntity } from "@/utils/grading.be";
|
import {
|
||||||
|
getGradingSystemByEntities,
|
||||||
|
getGradingSystemByEntity,
|
||||||
|
} from "@/utils/grading.be";
|
||||||
import { Grading } from "@/interfaces";
|
import { Grading } from "@/interfaces";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"]))
|
if (
|
||||||
return redirect("/")
|
shouldRedirectHome(user) ||
|
||||||
|
!checkAccess(user, [
|
||||||
|
"admin",
|
||||||
|
"developer",
|
||||||
|
"corporate",
|
||||||
|
"teacher",
|
||||||
|
"mastercorporate",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return redirect("/");
|
||||||
|
const [permissions, entities, allUsers] = await Promise.all([
|
||||||
|
getUserPermissions(user.id),
|
||||||
|
isAdmin(user)
|
||||||
|
? await getEntitiesWithRoles()
|
||||||
|
: await getEntitiesWithRoles(mapBy(user.entities, "id")),
|
||||||
|
getUsers(),
|
||||||
|
]);
|
||||||
|
const gradingSystems = await getGradingSystemByEntities(
|
||||||
|
mapBy(entities, "id")
|
||||||
|
);
|
||||||
|
const entitiesGrading = entities.map(
|
||||||
|
(e) =>
|
||||||
|
gradingSystems.find((g) => g.entity === e.id) || {
|
||||||
|
entity: e.id,
|
||||||
|
steps: CEFR_STEPS,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const permissions = await getUserPermissions(user.id);
|
return {
|
||||||
const entities = isAdmin(user) ? await getEntitiesWithRoles() : await getEntitiesWithRoles(mapBy(user.entities, 'id'))
|
props: serialize({
|
||||||
const allUsers = await getUsers()
|
user,
|
||||||
const gradingSystems = await getGradingSystemByEntities(mapBy(entities, 'id'))
|
permissions,
|
||||||
const entitiesGrading = entities.map(e => gradingSystems.find(g => g.entity === e.id) || { entity: e.id, steps: CEFR_STEPS })
|
entities,
|
||||||
|
allUsers,
|
||||||
return {
|
entitiesGrading,
|
||||||
props: serialize({ user, permissions, entities, allUsers, entitiesGrading }),
|
}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
permissions: PermissionType[];
|
permissions: PermissionType[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
allUsers: User[]
|
allUsers: User[];
|
||||||
entitiesGrading: Grading[]
|
entitiesGrading: Grading[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Admin({ user, entities, permissions, allUsers, entitiesGrading }: Props) {
|
export default function Admin({
|
||||||
const [modalOpen, setModalOpen] = useState<string>();
|
user,
|
||||||
const router = useRouter()
|
entities,
|
||||||
|
permissions,
|
||||||
|
allUsers,
|
||||||
|
entitiesGrading,
|
||||||
|
}: Props) {
|
||||||
|
const [modalOpen, setModalOpen] = useState<string>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const entitiesAllowCreateUser = useAllowedEntities(user, entities, 'create_user')
|
const entitiesAllowCreateUser = useAllowedEntities(
|
||||||
const entitiesAllowCreateUsers = useAllowedEntities(user, entities, 'create_user_batch')
|
user,
|
||||||
const entitiesAllowCreateCode = useAllowedEntities(user, entities, 'create_code')
|
entities,
|
||||||
const entitiesAllowCreateCodes = useAllowedEntities(user, entities, 'create_code_batch')
|
"create_user"
|
||||||
const entitiesAllowEditGrading = useAllowedEntities(user, entities, 'edit_grading_system')
|
);
|
||||||
|
const entitiesAllowCreateUsers = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"create_user_batch"
|
||||||
|
);
|
||||||
|
const entitiesAllowCreateCode = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"create_code"
|
||||||
|
);
|
||||||
|
const entitiesAllowCreateCodes = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"create_code_batch"
|
||||||
|
);
|
||||||
|
const entitiesAllowEditGrading = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"edit_grading_system"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Settings Panel | EnCoach</title>
|
<title>Settings Panel | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)} maxWidth="max-w-[85%]">
|
<Modal
|
||||||
<BatchCreateUser
|
isOpen={modalOpen === "batchCreateUser"}
|
||||||
user={user}
|
onClose={() => setModalOpen(undefined)}
|
||||||
entities={entitiesAllowCreateUser}
|
maxWidth="max-w-[85%]"
|
||||||
permissions={permissions}
|
>
|
||||||
onFinish={() => setModalOpen(undefined)}
|
<BatchCreateUser
|
||||||
/>
|
user={user}
|
||||||
</Modal>
|
entities={entitiesAllowCreateUser}
|
||||||
<Modal isOpen={modalOpen === "batchCreateCode"} onClose={() => setModalOpen(undefined)}>
|
permissions={permissions}
|
||||||
<BatchCodeGenerator
|
onFinish={() => setModalOpen(undefined)}
|
||||||
entities={entitiesAllowCreateCodes}
|
/>
|
||||||
user={user}
|
</Modal>
|
||||||
users={allUsers}
|
<Modal
|
||||||
permissions={permissions}
|
isOpen={modalOpen === "batchCreateCode"}
|
||||||
onFinish={() => setModalOpen(undefined)}
|
onClose={() => setModalOpen(undefined)}
|
||||||
/>
|
>
|
||||||
</Modal>
|
<BatchCodeGenerator
|
||||||
<Modal isOpen={modalOpen === "createCode"} onClose={() => setModalOpen(undefined)}>
|
entities={entitiesAllowCreateCodes}
|
||||||
<CodeGenerator
|
user={user}
|
||||||
entities={entitiesAllowCreateCode}
|
users={allUsers}
|
||||||
user={user}
|
permissions={permissions}
|
||||||
permissions={permissions}
|
onFinish={() => setModalOpen(undefined)}
|
||||||
onFinish={() => setModalOpen(undefined)}
|
/>
|
||||||
/>
|
</Modal>
|
||||||
</Modal>
|
<Modal
|
||||||
<Modal isOpen={modalOpen === "createUser"} onClose={() => setModalOpen(undefined)}>
|
isOpen={modalOpen === "createCode"}
|
||||||
<UserCreator
|
onClose={() => setModalOpen(undefined)}
|
||||||
user={user}
|
>
|
||||||
entities={entitiesAllowCreateUsers}
|
<CodeGenerator
|
||||||
users={allUsers}
|
entities={entitiesAllowCreateCode}
|
||||||
permissions={permissions}
|
user={user}
|
||||||
onFinish={() => setModalOpen(undefined)}
|
permissions={permissions}
|
||||||
/>
|
onFinish={() => setModalOpen(undefined)}
|
||||||
</Modal>
|
/>
|
||||||
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
|
</Modal>
|
||||||
<CorporateGradingSystem
|
<Modal
|
||||||
user={user}
|
isOpen={modalOpen === "createUser"}
|
||||||
entitiesGrading={entitiesGrading}
|
onClose={() => setModalOpen(undefined)}
|
||||||
entities={entitiesAllowEditGrading}
|
>
|
||||||
mutate={() => router.replace(router.asPath)}
|
<UserCreator
|
||||||
/>
|
user={user}
|
||||||
</Modal>
|
entities={entitiesAllowCreateUsers}
|
||||||
|
users={allUsers}
|
||||||
|
permissions={permissions}
|
||||||
|
onFinish={() => setModalOpen(undefined)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={modalOpen === "gradingSystem"}
|
||||||
|
onClose={() => setModalOpen(undefined)}
|
||||||
|
>
|
||||||
|
<CorporateGradingSystem
|
||||||
|
user={user}
|
||||||
|
entitiesGrading={entitiesGrading}
|
||||||
|
entities={entitiesAllowEditGrading}
|
||||||
|
mutate={() => router.replace(router.asPath)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8">
|
<section className="w-full grid grid-cols-2 -md:grid-cols-1 gap-8">
|
||||||
<ExamLoader />
|
<ExamLoader />
|
||||||
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && (
|
{checkAccess(
|
||||||
<div className="w-full grid grid-cols-2 gap-4">
|
user,
|
||||||
<IconCard
|
getTypesOfUser(["teacher"]),
|
||||||
Icon={BsCode}
|
permissions,
|
||||||
label="Generate Single Code"
|
"viewCodes"
|
||||||
color="purple"
|
) && (
|
||||||
className="w-full h-full"
|
<div className="w-full grid grid-cols-2 gap-4">
|
||||||
onClick={() => setModalOpen("createCode")}
|
<IconCard
|
||||||
disabled={entitiesAllowCreateCode.length === 0}
|
Icon={BsCode}
|
||||||
/>
|
label="Generate Single Code"
|
||||||
<IconCard
|
color="purple"
|
||||||
Icon={BsCodeSquare}
|
className="w-full h-full"
|
||||||
label="Generate Codes in Batch"
|
onClick={() => setModalOpen("createCode")}
|
||||||
color="purple"
|
disabled={entitiesAllowCreateCode.length === 0}
|
||||||
className="w-full h-full"
|
/>
|
||||||
onClick={() => setModalOpen("batchCreateCode")}
|
<IconCard
|
||||||
disabled={entitiesAllowCreateCodes.length === 0}
|
Icon={BsCodeSquare}
|
||||||
/>
|
label="Generate Codes in Batch"
|
||||||
<IconCard
|
color="purple"
|
||||||
Icon={BsPersonFill}
|
className="w-full h-full"
|
||||||
label="Create Single User"
|
onClick={() => setModalOpen("batchCreateCode")}
|
||||||
color="purple"
|
disabled={entitiesAllowCreateCodes.length === 0}
|
||||||
className="w-full h-full"
|
/>
|
||||||
onClick={() => setModalOpen("createUser")}
|
<IconCard
|
||||||
disabled={entitiesAllowCreateUser.length === 0}
|
Icon={BsPersonFill}
|
||||||
/>
|
label="Create Single User"
|
||||||
<IconCard
|
color="purple"
|
||||||
Icon={BsPeopleFill}
|
className="w-full h-full"
|
||||||
label="Create Users in Batch"
|
onClick={() => setModalOpen("createUser")}
|
||||||
color="purple"
|
disabled={entitiesAllowCreateUser.length === 0}
|
||||||
className="w-full h-full"
|
/>
|
||||||
onClick={() => setModalOpen("batchCreateUser")}
|
<IconCard
|
||||||
disabled={entitiesAllowCreateUsers.length === 0}
|
Icon={BsPeopleFill}
|
||||||
/>
|
label="Create Users in Batch"
|
||||||
{checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && (
|
color="purple"
|
||||||
<IconCard
|
className="w-full h-full"
|
||||||
Icon={BsGearFill}
|
onClick={() => setModalOpen("batchCreateUser")}
|
||||||
label="Grading System"
|
disabled={entitiesAllowCreateUsers.length === 0}
|
||||||
color="purple"
|
/>
|
||||||
className="w-full h-full col-span-2"
|
{checkAccess(user, [
|
||||||
onClick={() => setModalOpen("gradingSystem")}
|
"admin",
|
||||||
disabled={entitiesAllowEditGrading.length === 0}
|
"corporate",
|
||||||
/>
|
"developer",
|
||||||
)}
|
"mastercorporate",
|
||||||
</div>
|
]) && (
|
||||||
)}
|
<IconCard
|
||||||
</section>
|
Icon={BsGearFill}
|
||||||
<section className="w-full">
|
label="Grading System"
|
||||||
<Lists user={user} entities={entities} permissions={permissions} />
|
color="purple"
|
||||||
</section>
|
className="w-full h-full col-span-2"
|
||||||
</>
|
onClick={() => setModalOpen("gradingSystem")}
|
||||||
</>
|
disabled={entitiesAllowEditGrading.length === 0}
|
||||||
);
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<section className="w-full">
|
||||||
|
<Lists user={user} entities={entities} permissions={permissions} />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import { Session } from "@/hooks/useSessions";
|
|||||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import { Exam } from "@/interfaces/exam";
|
||||||
import { Assignment, AssignmentResult } from "@/interfaces/results";
|
import { Assignment, AssignmentResult } from "@/interfaces/results";
|
||||||
import { StudentUser, User } from "@/interfaces/user";
|
import { StudentUser, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getEntitiesAssignments } from "@/utils/assignments.be";
|
import { getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getExamsByIds } from "@/utils/exams.be";
|
import { getExamsByIds } from "@/utils/exams.be";
|
||||||
import { findAllowedEntities } from "@/utils/permissions";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getSessionsByAssignments } from "@/utils/sessions.be";
|
import { getSessionsByAssignments } from "@/utils/sessions.be";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
@@ -28,278 +28,399 @@ import Head from "next/head";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import {
|
import { BsBank, BsChevronLeft, BsX } from "react-icons/bs";
|
||||||
BsBank,
|
|
||||||
BsChevronLeft,
|
|
||||||
BsX,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: StudentUser[];
|
students: StudentUser[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
sessions: Session[]
|
sessions: Session[];
|
||||||
exams: Exam[]
|
exams: Exam[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
|
const entities = await getEntitiesWithRoles(
|
||||||
const allowedEntities = findAllowedEntities(user, entities, 'view_entity_statistics')
|
isAdmin(user) ? undefined : entityIDS
|
||||||
|
);
|
||||||
|
const allowedEntities = findAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"view_entity_statistics"
|
||||||
|
);
|
||||||
|
|
||||||
if (allowedEntities.length === 0) return redirect("/")
|
if (allowedEntities.length === 0) return redirect("/");
|
||||||
|
|
||||||
const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students')
|
const studentsAllowedEntities = findAllowedEntities(
|
||||||
const students = await getEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" })
|
user,
|
||||||
|
entities,
|
||||||
|
"view_students"
|
||||||
|
);
|
||||||
|
|
||||||
const assignments = await getEntitiesAssignments(mapBy(entities, "id"));
|
const [students, assignments] = await Promise.all([
|
||||||
const sessions = await getSessionsByAssignments(mapBy(assignments, 'id'))
|
getEntitiesUsers(mapBy(studentsAllowedEntities, "id"), { type: "student" }),
|
||||||
const exams = await getExamsByIds(assignments.flatMap(a => a.exams))
|
getEntitiesAssignments(mapBy(entities, "id")),
|
||||||
|
]);
|
||||||
|
|
||||||
return { props: serialize({ user, students, entities: allowedEntities, assignments, sessions, exams }) };
|
const [sessions, exams] = await Promise.all([
|
||||||
|
getSessionsByAssignments(mapBy(assignments, "id")),
|
||||||
|
getExamsByIds(assignments.flatMap((a) => a.exams)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
students,
|
||||||
|
entities: allowedEntities,
|
||||||
|
assignments,
|
||||||
|
sessions,
|
||||||
|
exams,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
student: StudentUser
|
student: StudentUser;
|
||||||
result?: AssignmentResult
|
result?: AssignmentResult;
|
||||||
assignment: Assignment
|
assignment: Assignment;
|
||||||
exams: Exam[]
|
exams: Exam[];
|
||||||
entity: Entity
|
entity: Entity;
|
||||||
session?: Session
|
session?: Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<Item>();
|
const columnHelper = createColumnHelper<Item>();
|
||||||
|
|
||||||
export default function Statistical({ user, students, entities, assignments, sessions, exams }: Props) {
|
export default function Statistical({
|
||||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
user,
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment().add(1, 'month').toDate());
|
students,
|
||||||
const [selectedEntities, setSelectedEntities] = useState<string[]>([])
|
entities,
|
||||||
const [isDownloading, setIsDownloading] = useState(false)
|
assignments,
|
||||||
|
sessions,
|
||||||
|
exams,
|
||||||
|
}: Props) {
|
||||||
|
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||||
|
const [endDate, setEndDate] = useState<Date | null>(
|
||||||
|
moment().add(1, "month").toDate()
|
||||||
|
);
|
||||||
|
const [selectedEntities, setSelectedEntities] = useState<string[]>([]);
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
|
||||||
const entitiesAllowDownload = useAllowedEntities(user, entities, 'download_statistics_report')
|
const entitiesAllowDownload = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"download_statistics_report"
|
||||||
|
);
|
||||||
|
|
||||||
const resetDateRange = () => {
|
const resetDateRange = () => {
|
||||||
const orderedAssignments = orderBy(assignments, ['startDate'], ['asc'])
|
const orderedAssignments = orderBy(assignments, ["startDate"], ["asc"]);
|
||||||
setStartDate(moment(orderedAssignments.shift()?.startDate || "2024-01-01T00:00:01.986Z").toDate())
|
setStartDate(
|
||||||
setEndDate(moment().add(1, 'month').toDate())
|
moment(
|
||||||
}
|
orderedAssignments.shift()?.startDate || "2024-01-01T00:00:01.986Z"
|
||||||
|
).toDate()
|
||||||
|
);
|
||||||
|
setEndDate(moment().add(1, "month").toDate());
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(resetDateRange, [assignments])
|
useEffect(resetDateRange, [assignments]);
|
||||||
|
|
||||||
const updateDateRange = (dates: [Date, Date | null]) => {
|
const updateDateRange = (dates: [Date, Date | null]) => {
|
||||||
const [start, end] = dates;
|
const [start, end] = dates;
|
||||||
setStartDate(start!);
|
setStartDate(start!);
|
||||||
setEndDate(end);
|
setEndDate(end);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleEntity = (id: string) => setSelectedEntities(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
const toggleEntity = (id: string) =>
|
||||||
|
setSelectedEntities((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||||
|
);
|
||||||
|
|
||||||
const renderAssignmentResolution = (entityID: string) => {
|
const renderAssignmentResolution = (entityID: string) => {
|
||||||
const entityAssignments = filterBy(assignments, 'entity', entityID)
|
const entityAssignments = filterBy(assignments, "entity", entityID);
|
||||||
const total = entityAssignments.reduce((acc, curr) => acc + curr.assignees.length, 0)
|
const total = entityAssignments.reduce(
|
||||||
const results = entityAssignments.reduce((acc, curr) => acc + curr.results.length, 0)
|
(acc, curr) => acc + curr.assignees.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const results = entityAssignments.reduce(
|
||||||
|
(acc, curr) => acc + curr.results.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
return `${results}/${total}`
|
return `${results}/${total}`;
|
||||||
}
|
};
|
||||||
|
|
||||||
const totalAssignmentResolution = useMemo(() => {
|
const totalAssignmentResolution = useMemo(() => {
|
||||||
const total = assignments.reduce((acc, curr) => acc + curr.assignees.length, 0)
|
const total = assignments.reduce(
|
||||||
const results = assignments.reduce((acc, curr) => acc + curr.results.length, 0)
|
(acc, curr) => acc + curr.assignees.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const results = assignments.reduce(
|
||||||
|
(acc, curr) => acc + curr.results.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
return { results, total }
|
return { results, total };
|
||||||
}, [assignments])
|
}, [assignments]);
|
||||||
|
|
||||||
const filteredAssignments = useMemo(() => {
|
const filteredAssignments = useMemo(() => {
|
||||||
if (!startDate && !endDate) return assignments
|
if (!startDate && !endDate) return assignments;
|
||||||
const startDateFiltered = startDate ? assignments.filter(a => moment(a.startDate).isSameOrAfter(moment(startDate))) : assignments
|
const startDateFiltered = startDate
|
||||||
return endDate ? startDateFiltered.filter(a => moment(a.endDate).isSameOrBefore(moment(endDate))) : startDateFiltered
|
? assignments.filter((a) =>
|
||||||
}, [startDate, endDate, assignments])
|
moment(a.startDate).isSameOrAfter(moment(startDate))
|
||||||
|
)
|
||||||
|
: assignments;
|
||||||
|
return endDate
|
||||||
|
? startDateFiltered.filter((a) =>
|
||||||
|
moment(a.endDate).isSameOrBefore(moment(endDate))
|
||||||
|
)
|
||||||
|
: startDateFiltered;
|
||||||
|
}, [startDate, endDate, assignments]);
|
||||||
|
|
||||||
const data: Item[] = useMemo(() =>
|
const data: Item[] = useMemo(
|
||||||
filteredAssignments.filter(a => selectedEntities.includes(a.entity || "")).flatMap(a => a.assignees.map(x => {
|
() =>
|
||||||
const result = findBy(a.results, 'user', x)
|
filteredAssignments
|
||||||
const student = findBy(students, 'id', x)
|
.filter((a) => selectedEntities.includes(a.entity || ""))
|
||||||
const entity = findBy(entities, 'id', a.entity)
|
.flatMap((a) =>
|
||||||
const assignmentExams = exams.filter(e => a.exams.map(x => `${x.id}_${x.module}`).includes(`${e.id}_${e.module}`))
|
a.assignees.map((x) => {
|
||||||
const session = sessions.find(s => s.assignment?.id === a.id && s.user === x)
|
const result = findBy(a.results, "user", x);
|
||||||
|
const student = findBy(students, "id", x);
|
||||||
|
const entity = findBy(entities, "id", a.entity);
|
||||||
|
const assignmentExams = exams.filter((e) =>
|
||||||
|
a.exams
|
||||||
|
.map((x) => `${x.id}_${x.module}`)
|
||||||
|
.includes(`${e.id}_${e.module}`)
|
||||||
|
);
|
||||||
|
const session = sessions.find(
|
||||||
|
(s) => s.assignment?.id === a.id && s.user === x
|
||||||
|
);
|
||||||
|
|
||||||
if (!student) return undefined
|
if (!student) return undefined;
|
||||||
return { student, result, assignment: a, exams: assignmentExams, session, entity }
|
return {
|
||||||
})).filter(x => !!x) as Item[],
|
student,
|
||||||
[students, selectedEntities, filteredAssignments, exams, sessions, entities]
|
result,
|
||||||
)
|
assignment: a,
|
||||||
|
exams: assignmentExams,
|
||||||
|
session,
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter((x) => !!x) as Item[],
|
||||||
|
[students, selectedEntities, filteredAssignments, exams, sessions, entities]
|
||||||
|
);
|
||||||
|
|
||||||
const sortedData: Item[] = useMemo(() => data.sort((a, b) => {
|
const sortedData: Item[] = useMemo(
|
||||||
const aTotalScore = a.result?.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0) || 0
|
() =>
|
||||||
const bTotalScore = b.result?.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0) || 0
|
data.sort((a, b) => {
|
||||||
|
const aTotalScore =
|
||||||
|
a.result?.stats
|
||||||
|
.filter((x) => !x.isPractice)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0) || 0;
|
||||||
|
const bTotalScore =
|
||||||
|
b.result?.stats
|
||||||
|
.filter((x) => !x.isPractice)
|
||||||
|
.reduce((acc, curr) => acc + curr.score.correct, 0) || 0;
|
||||||
|
|
||||||
return bTotalScore - aTotalScore
|
return bTotalScore - aTotalScore;
|
||||||
}), [data])
|
}),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
const downloadExcel = async () => {
|
const downloadExcel = async () => {
|
||||||
setIsDownloading(true)
|
setIsDownloading(true);
|
||||||
|
|
||||||
const request = await axios.post("/api/statistical", {
|
const request = await axios.post(
|
||||||
entities: entities.filter(e => selectedEntities.includes(e.id)),
|
"/api/statistical",
|
||||||
items: data,
|
{
|
||||||
assignments: filteredAssignments,
|
entities: entities.filter((e) => selectedEntities.includes(e.id)),
|
||||||
startDate,
|
items: data,
|
||||||
endDate
|
assignments: filteredAssignments,
|
||||||
}, {
|
startDate,
|
||||||
responseType: 'blob'
|
endDate,
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
responseType: "blob",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const href = URL.createObjectURL(request.data)
|
const href = URL.createObjectURL(request.data);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement("a");
|
||||||
link.href = href;
|
link.href = href;
|
||||||
link.setAttribute('download', `statistical_${new Date().toISOString()}.xlsx`);
|
link.setAttribute(
|
||||||
document.body.appendChild(link);
|
"download",
|
||||||
link.click();
|
`statistical_${new Date().toISOString()}.xlsx`
|
||||||
|
);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(href);
|
URL.revokeObjectURL(href);
|
||||||
|
|
||||||
setIsDownloading(false)
|
setIsDownloading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor("student.name", {
|
columnHelper.accessor("student.name", {
|
||||||
header: "Student",
|
header: "Student",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("student.studentID", {
|
columnHelper.accessor("student.studentID", {
|
||||||
header: "Student ID",
|
header: "Student ID",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("student.email", {
|
columnHelper.accessor("student.email", {
|
||||||
header: "E-mail",
|
header: "E-mail",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("entity.label", {
|
columnHelper.accessor("entity.label", {
|
||||||
header: "Entity",
|
header: "Entity",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("assignment.name", {
|
columnHelper.accessor("assignment.name", {
|
||||||
header: "Assignment",
|
header: "Assignment",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("assignment.startDate", {
|
columnHelper.accessor("assignment.startDate", {
|
||||||
header: "Date",
|
header: "Date",
|
||||||
cell: (info) => moment(info.getValue()).format("DD/MM/YYYY"),
|
cell: (info) => moment(info.getValue()).format("DD/MM/YYYY"),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("result", {
|
columnHelper.accessor("result", {
|
||||||
header: "Progress",
|
header: "Progress",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const student = info.row.original.student
|
const student = info.row.original.student;
|
||||||
const session = info.row.original.session
|
const session = info.row.original.session;
|
||||||
|
|
||||||
if (!student.lastLogin) return <span className="text-mti-red-dark">Never logged in</span>
|
if (!student.lastLogin)
|
||||||
if (info.getValue()) return <span className="text-mti-green font-semibold">Submitted</span>
|
return <span className="text-mti-red-dark">Never logged in</span>;
|
||||||
if (!session) return <span className="text-mti-rose">Not started</span>
|
if (info.getValue())
|
||||||
|
return (
|
||||||
|
<span className="text-mti-green font-semibold">Submitted</span>
|
||||||
|
);
|
||||||
|
if (!session) return <span className="text-mti-rose">Not started</span>;
|
||||||
|
|
||||||
return <span className="font-semibold">
|
return (
|
||||||
{capitalize(session.exam?.module || "")} Module, Part {session.partIndex + 1}, Exercise {session.exerciseIndex + 1}
|
<span className="font-semibold">
|
||||||
</span>
|
{capitalize(session.exam?.module || "")} Module, Part{" "}
|
||||||
},
|
{session.partIndex + 1}, Exercise {session.exerciseIndex + 1}
|
||||||
})
|
</span>
|
||||||
]
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Statistical | EnCoach</title>
|
<title>Statistical | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/dashboard" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link
|
||||||
<BsChevronLeft />
|
href="/dashboard"
|
||||||
</Link>
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
<h2 className="font-bold text-2xl">Statistical</h2>
|
>
|
||||||
</div>
|
<BsChevronLeft />
|
||||||
<Checkbox
|
</Link>
|
||||||
onChange={value => setSelectedEntities(value ? mapBy(entities, 'id') : [])}
|
<h2 className="font-bold text-2xl">Statistical</h2>
|
||||||
isChecked={selectedEntities.length === entities.length}
|
</div>
|
||||||
>
|
<Checkbox
|
||||||
Select All
|
onChange={(value) =>
|
||||||
</Checkbox>
|
setSelectedEntities(value ? mapBy(entities, "id") : [])
|
||||||
</div>
|
}
|
||||||
<Separator />
|
isChecked={selectedEntities.length === entities.length}
|
||||||
</div>
|
>
|
||||||
|
Select All
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<div className="w-full flex items-center justify-between gap-4 flex-wrap">
|
<div className="w-full flex items-center justify-between gap-4 flex-wrap">
|
||||||
{entities.map(entity => (
|
{entities.map((entity) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleEntity(entity.id)}
|
onClick={() => toggleEntity(entity.id)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col items-center justify-between gap-3 border-2 drop-shadow rounded-xl bg-white p-8 px-2 w-48 h-52",
|
"flex flex-col items-center justify-between gap-3 border-2 drop-shadow rounded-xl bg-white p-8 px-2 w-48 h-52",
|
||||||
"transition ease-in-out duration-300 hover:shadow-xl hover:border-mti-purple",
|
"transition ease-in-out duration-300 hover:shadow-xl hover:border-mti-purple",
|
||||||
selectedEntities.includes(entity.id) && "border-mti-purple text-mti-purple"
|
selectedEntities.includes(entity.id) &&
|
||||||
)}
|
"border-mti-purple text-mti-purple"
|
||||||
key={entity.id}
|
)}
|
||||||
>
|
key={entity.id}
|
||||||
<BsBank size={48} />
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<BsBank size={48} />
|
||||||
<span>{entity.label}</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className={clsx("font-semibold")}>
|
<span>{entity.label}</span>
|
||||||
{renderAssignmentResolution(entity.id)}
|
<span className={clsx("font-semibold")}>
|
||||||
</span>
|
{renderAssignmentResolution(entity.id)}
|
||||||
</div>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
))}
|
</button>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 px-12 w-full flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 px-12 w-full flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip",
|
"hover:border-mti-purple tooltip",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
selectsRange
|
selectsRange
|
||||||
selected={startDate}
|
selected={startDate}
|
||||||
onChange={updateDateRange}
|
onChange={updateDateRange}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
/>
|
/>
|
||||||
{startDate !== null && endDate !== null && (
|
{startDate !== null && endDate !== null && (
|
||||||
<button onClick={resetDateRange} className="transition ease-in-out duration-300 rounded-full p-2 hover:bg-mti-gray-cool/10">
|
<button
|
||||||
<BsX size={24} />
|
onClick={resetDateRange}
|
||||||
</button>
|
className="transition ease-in-out duration-300 rounded-full p-2 hover:bg-mti-gray-cool/10"
|
||||||
)}
|
>
|
||||||
</div>
|
<BsX size={24} />
|
||||||
<span className="font-semibold text-lg pr-1">
|
</button>
|
||||||
Total: {totalAssignmentResolution.results} / {totalAssignmentResolution.total}
|
)}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<span className="font-semibold text-lg pr-1">
|
||||||
</section>
|
Total: {totalAssignmentResolution.results} /{" "}
|
||||||
|
{totalAssignmentResolution.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{selectedEntities.length > 0 && (
|
{selectedEntities.length > 0 && (
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={sortedData}
|
data={sortedData}
|
||||||
searchFields={[["student", "name"], ["student", "email"], ["student", "studentID"], ["exams", "id"], ["assignment", "name"]]}
|
searchFields={[
|
||||||
searchPlaceholder="Search by student, assignment or exam..."
|
["student", "name"],
|
||||||
onDownload={entitiesAllowDownload.length > 0 ? downloadExcel : undefined}
|
["student", "email"],
|
||||||
isDownloadLoading={isDownloading}
|
["student", "studentID"],
|
||||||
/>
|
["exams", "id"],
|
||||||
)}
|
["assignment", "name"],
|
||||||
</>
|
]}
|
||||||
</>
|
searchPlaceholder="Search by student, assignment or exam..."
|
||||||
)
|
onDownload={
|
||||||
|
entitiesAllowDownload.length > 0 ? downloadExcel : undefined
|
||||||
|
}
|
||||||
|
isDownloadLoading={isDownloading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ import { capitalize } from "lodash";
|
|||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import { calculateBandScore } from "@/utils/score";
|
import { calculateBandScore } from "@/utils/score";
|
||||||
import {
|
import { MODULE_ARRAY, sortByModule } from "@/utils/moduleUtils";
|
||||||
MODULE_ARRAY,
|
|
||||||
sortByModule,
|
|
||||||
} from "@/utils/moduleUtils";
|
|
||||||
import { Chart } from "react-chartjs-2";
|
import { Chart } from "react-chartjs-2";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
@@ -45,7 +42,6 @@ import { Stat, User } from "@/interfaces/user";
|
|||||||
import { Divider } from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import Badge from "@/components/Low/Badge";
|
import Badge from "@/components/Low/Badge";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntities } from "@/utils/entities.be";
|
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
@@ -69,19 +65,10 @@ const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"];
|
|||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res);
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login");
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/");
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, "id");
|
|
||||||
const isAdmin = checkAccess(user, ["admin", "developer"]);
|
const isAdmin = checkAccess(user, ["admin", "developer"]);
|
||||||
|
|
||||||
const entities = await getEntities(isAdmin ? undefined : entityIDs, {
|
|
||||||
id: 1,
|
|
||||||
label: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, entities, isAdmin }),
|
props: serialize({ user, isAdmin }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -1,225 +1,269 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {User} from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import {ToastContainer} from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {FaPlus} from "react-icons/fa";
|
import { FaPlus } from "react-icons/fa";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {ITrainingContent} from "@/training/TrainingInterfaces";
|
import { ITrainingContent } from "@/training/TrainingInterfaces";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {uuidv4} from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import TrainingScore from "@/training/TrainingScore";
|
import TrainingScore from "@/training/TrainingScore";
|
||||||
import ModuleBadge from "@/components/ModuleBadge";
|
import ModuleBadge from "@/components/ModuleBadge";
|
||||||
import RecordFilter from "@/components/Medium/RecordFilter";
|
import RecordFilter from "@/components/Medium/RecordFilter";
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
import { checkAccess } from "../../utils/permissions";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
const isAdmin = checkAccess(user, ["admin", "developer"]);
|
||||||
|
const entityIDs = mapBy(user.entities, "id");
|
||||||
|
const users = await getEntitiesUsers(entityIDs);
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, 'id')
|
return {
|
||||||
const entities = await getEntitiesWithRoles(entityIDs)
|
props: serialize({ user, users, isAdmin }),
|
||||||
const users = await getEntitiesUsers(entityIDs)
|
};
|
||||||
|
|
||||||
return {
|
|
||||||
props: serialize({user, users, entities}),
|
|
||||||
};
|
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[] }> = ({user, entities, users}) => {
|
const Training: React.FC<{
|
||||||
const [recordUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setTraining]);
|
user: User;
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
entities: EntityWithRoles[];
|
||||||
|
users: User[];
|
||||||
|
isAdmin: boolean;
|
||||||
|
}> = ({ user, entities, isAdmin }) => {
|
||||||
|
const [recordUserId, setRecordTraining] = useRecordStore((state) => [
|
||||||
|
state.selectedUser,
|
||||||
|
state.setTraining,
|
||||||
|
]);
|
||||||
|
const [filter, setFilter] = useState<
|
||||||
|
"months" | "weeks" | "days" | "assignments"
|
||||||
|
>();
|
||||||
|
|
||||||
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
|
const [stats, setTrainingStats] = useTrainingContentStore((state) => [
|
||||||
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
|
state.stats,
|
||||||
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{[key: string]: ITrainingContent}>();
|
state.setStats,
|
||||||
|
]);
|
||||||
|
const [isNewContentLoading, setIsNewContentLoading] = useState(
|
||||||
|
stats.length != 0
|
||||||
|
);
|
||||||
|
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{
|
||||||
|
[key: string]: ITrainingContent;
|
||||||
|
}>();
|
||||||
|
|
||||||
const {data: trainingContent, isLoading: areRecordsLoading} = useFilterRecordsByUser<ITrainingContent[]>(
|
const { data: trainingContent, isLoading: areRecordsLoading } =
|
||||||
recordUserId || user?.id,
|
useFilterRecordsByUser<ITrainingContent[]>(
|
||||||
undefined,
|
recordUserId || user?.id,
|
||||||
"training",
|
undefined,
|
||||||
);
|
"training"
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
setTrainingStats([]);
|
setTrainingStats([]);
|
||||||
};
|
};
|
||||||
router.events.on("routeChangeStart", handleRouteChange);
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeStart", handleRouteChange);
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [router.events, setTrainingStats]);
|
}, [router.events, setTrainingStats]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const postStats = async () => {
|
const postStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<{id: string}>(`/api/training`, {userID: user.id, stats: stats});
|
const response = await axios.post<{ id: string }>(`/api/training`, {
|
||||||
return response.data.id;
|
userID: user.id,
|
||||||
} catch (error) {
|
stats: stats,
|
||||||
setIsNewContentLoading(false);
|
});
|
||||||
}
|
return response.data.id;
|
||||||
};
|
} catch (error) {
|
||||||
|
setIsNewContentLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isNewContentLoading) {
|
if (isNewContentLoading) {
|
||||||
postStats().then((id) => {
|
postStats().then((id) => {
|
||||||
setTrainingStats([]);
|
setTrainingStats([]);
|
||||||
if (id) {
|
if (id) {
|
||||||
router.push(`/training/${id}`);
|
router.push(`/training/${id}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isNewContentLoading]);
|
}, [isNewContentLoading]);
|
||||||
|
|
||||||
const handleNewTrainingContent = () => {
|
const handleNewTrainingContent = () => {
|
||||||
setRecordTraining(true);
|
setRecordTraining(true);
|
||||||
router.push("/record");
|
router.push("/record");
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
|
const filterTrainingContentByDate = (trainingContent: {
|
||||||
if (filter) {
|
[key: string]: ITrainingContent;
|
||||||
const filterDate = moment()
|
}) => {
|
||||||
.subtract({[filter as string]: 1})
|
if (filter) {
|
||||||
.format("x");
|
const filterDate = moment()
|
||||||
const filteredTrainingContent: {[key: string]: ITrainingContent} = {};
|
.subtract({ [filter as string]: 1 })
|
||||||
|
.format("x");
|
||||||
|
const filteredTrainingContent: { [key: string]: ITrainingContent } = {};
|
||||||
|
|
||||||
Object.keys(trainingContent).forEach((timestamp) => {
|
Object.keys(trainingContent).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
if (timestamp >= filterDate)
|
||||||
});
|
filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
||||||
return filteredTrainingContent;
|
});
|
||||||
}
|
return filteredTrainingContent;
|
||||||
return trainingContent;
|
}
|
||||||
};
|
return trainingContent;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (trainingContent.length > 0) {
|
if (trainingContent.length > 0) {
|
||||||
const grouped = trainingContent.reduce((acc, content) => {
|
const grouped = trainingContent.reduce((acc, content) => {
|
||||||
acc[content.created_at] = content;
|
acc[content.created_at] = content;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as {[key: number]: ITrainingContent});
|
}, {} as { [key: number]: ITrainingContent });
|
||||||
|
|
||||||
setGroupedByTrainingContent(grouped);
|
setGroupedByTrainingContent(grouped);
|
||||||
} else {
|
} else {
|
||||||
setGroupedByTrainingContent(undefined);
|
setGroupedByTrainingContent(undefined);
|
||||||
}
|
}
|
||||||
}, [trainingContent]);
|
}, [trainingContent]);
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) => {
|
const formatTimestamp = (timestamp: string) => {
|
||||||
const date = moment(parseInt(timestamp));
|
const date = moment(parseInt(timestamp));
|
||||||
const formatter = "YYYY/MM/DD - HH:mm";
|
const formatter = "YYYY/MM/DD - HH:mm";
|
||||||
|
|
||||||
return date.format(formatter);
|
return date.format(formatter);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
const selectTrainingContent = (trainingContent: ITrainingContent) => {
|
||||||
router.push(`/training/${trainingContent.id}`);
|
router.push(`/training/${trainingContent.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const trainingContentContainer = (timestamp: string) => {
|
const trainingContentContainer = (timestamp: string) => {
|
||||||
if (!groupedByTrainingContent) return <></>;
|
if (!groupedByTrainingContent) return <></>;
|
||||||
|
|
||||||
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
const trainingContent: ITrainingContent =
|
||||||
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
|
groupedByTrainingContent[timestamp];
|
||||||
|
const uniqueModules = [
|
||||||
|
...new Set(trainingContent.exams.map((exam) => exam.module)),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
key={uuidv4()}
|
key={uuidv4()}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden",
|
"flex flex-col justify-between gap-4 border border-mti-gray-platinum p-4 cursor-pointer rounded-xl transition ease-in-out duration-300 -md:hidden"
|
||||||
)}
|
)}
|
||||||
onClick={() => selectTrainingContent(trainingContent)}
|
onClick={() => selectTrainingContent(trainingContent)}
|
||||||
role="button">
|
role="button"
|
||||||
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
>
|
||||||
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
|
<div className="w-full flex justify-between -md:items-center 2xl:items-center">
|
||||||
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
<div className="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
|
||||||
</div>
|
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
||||||
<div className="flex flex-col gap-2">
|
</div>
|
||||||
<div className="w-full flex flex-row gap-1">
|
<div className="flex flex-col gap-2">
|
||||||
{uniqueModules.map((module) => (
|
<div className="w-full flex flex-row gap-1">
|
||||||
<ModuleBadge key={module} module={module} />
|
{uniqueModules.map((module) => (
|
||||||
))}
|
<ModuleBadge key={module} module={module} />
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TrainingScore trainingContent={trainingContent} gridView={true} />
|
</div>
|
||||||
</div>
|
<TrainingScore trainingContent={trainingContent} gridView={true} />
|
||||||
</>
|
</div>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Training | EnCoach</title>
|
<title>Training | EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<>
|
<>
|
||||||
{isNewContentLoading || areRecordsLoading ? (
|
{isNewContentLoading || areRecordsLoading ? (
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse 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 animate-pulse flex-col items-center gap-12">
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
{isNewContentLoading && (
|
{isNewContentLoading && (
|
||||||
<span className="text-center text-2xl font-bold text-mti-green-light">Assessing your exams, please be patient...</span>
|
<span className="text-center text-2xl font-bold text-mti-green-light">
|
||||||
)}
|
Assessing your exams, please be patient...
|
||||||
</div>
|
</span>
|
||||||
) : (
|
)}
|
||||||
<>
|
</div>
|
||||||
<RecordFilter entities={entities} user={user} filterState={{filter: filter, setFilter: setFilter}} assignments={false}>
|
) : (
|
||||||
{user.type === "student" && (
|
<>
|
||||||
<>
|
<RecordFilter
|
||||||
<div className="flex items-center">
|
entities={entities}
|
||||||
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
user={user}
|
||||||
<button
|
isAdmin={isAdmin}
|
||||||
className={clsx(
|
filterState={{ filter: filter, setFilter: setFilter }}
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4",
|
assignments={false}
|
||||||
"transition duration-300 ease-in-out",
|
>
|
||||||
)}
|
{user.type === "student" && (
|
||||||
onClick={handleNewTrainingContent}>
|
<>
|
||||||
<FaPlus />
|
<div className="flex items-center">
|
||||||
</button>
|
<div className="font-semibold text-2xl">
|
||||||
</div>
|
Generate New Training Material
|
||||||
</>
|
</div>
|
||||||
)}
|
<button
|
||||||
</RecordFilter>
|
className={clsx(
|
||||||
{trainingContent.length == 0 && (
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4",
|
||||||
<div className="flex flex-grow justify-center items-center">
|
"transition duration-300 ease-in-out"
|
||||||
<span className="font-semibold ml-1">No training content to display...</span>
|
)}
|
||||||
</div>
|
onClick={handleNewTrainingContent}
|
||||||
)}
|
>
|
||||||
{!areRecordsLoading && groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
<FaPlus />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
</button>
|
||||||
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
</div>
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
</>
|
||||||
.map(trainingContentContainer)}
|
)}
|
||||||
</div>
|
</RecordFilter>
|
||||||
)}
|
{trainingContent.length == 0 && (
|
||||||
</>
|
<div className="flex flex-grow justify-center items-center">
|
||||||
)}
|
<span className="font-semibold ml-1">
|
||||||
</>
|
No training content to display...
|
||||||
</>
|
</span>
|
||||||
);
|
</div>
|
||||||
|
)}
|
||||||
|
{!areRecordsLoading &&
|
||||||
|
groupedByTrainingContent &&
|
||||||
|
Object.keys(groupedByTrainingContent).length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||||
|
{Object.keys(
|
||||||
|
filterTrainingContentByDate(groupedByTrainingContent)
|
||||||
|
)
|
||||||
|
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
|
.map(trainingContentContainer)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Training;
|
export default Training;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import { Group, Stat, StudentUser, User } from "@/interfaces/user";
|
import { Group, Stat, StudentUser, User } from "@/interfaces/user";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BsChevronLeft } from "react-icons/bs";
|
import { BsChevronLeft } from "react-icons/bs";
|
||||||
import { mapBy, serialize } from "@/utils";
|
import { mapBy, serialize } from "@/utils";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { Entity } from "@/interfaces/entity";
|
import { Entity } from "@/interfaces/entity";
|
||||||
import { getParticipantsGroups } from "@/utils/groups.be";
|
import { getParticipantsGroups } from "@/utils/groups.be";
|
||||||
import StudentPerformanceList from "../(admin)/Lists/StudentPerformanceList";
|
import StudentPerformanceList from "../(admin)/Lists/StudentPerformanceList";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
@@ -17,73 +17,84 @@ import { requestUser } from "@/utils/api";
|
|||||||
import { redirect } from "@/utils";
|
import { redirect } from "@/utils";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, 'id')
|
const entityIDs = mapBy(user.entities, "id");
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", 'developer']) ? undefined : entityIDs)
|
const entities = await getEntitiesWithRoles(
|
||||||
const allowedEntities = findAllowedEntities(user, entities, "view_student_performance")
|
checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs
|
||||||
|
);
|
||||||
|
const allowedEntities = findAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"view_student_performance"
|
||||||
|
);
|
||||||
|
|
||||||
if (allowedEntities.length === 0) return redirect("/")
|
if (allowedEntities.length === 0) return redirect("/");
|
||||||
|
|
||||||
const students = await (checkAccess(user, ["admin", 'developer'])
|
const students = await (checkAccess(user, ["admin", "developer"])
|
||||||
? getUsers({ type: 'student' })
|
? getUsers({ type: "student" })
|
||||||
: getEntitiesUsers(mapBy(allowedEntities, 'id'), { type: 'student' })
|
: getEntitiesUsers(mapBy(allowedEntities, "id"), { type: "student" }));
|
||||||
)
|
const groups = await getParticipantsGroups(mapBy(students, "id"));
|
||||||
const groups = await getParticipantsGroups(mapBy(students, 'id'))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, students, entities, groups }),
|
props: serialize({ user, students, entities, groups }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: StudentUser[]
|
students: StudentUser[];
|
||||||
entities: Entity[]
|
entities: Entity[];
|
||||||
groups: Group[]
|
groups: Group[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
||||||
const { data: stats } = useFilterRecordsByUser<Stat[]>();
|
const { data: stats } = useFilterRecordsByUser<Stat[]>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const performanceStudents = students.map((u) => ({
|
const performanceStudents = students.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",
|
||||||
entitiesLabel: mapBy(u.entities, 'id').map((id) => entities.find((e) => e.id === id)?.label).filter((e) => !!e).join(', '),
|
entitiesLabel: mapBy(u.entities, "id")
|
||||||
}));
|
.map((id) => entities.find((e) => e.id === id)?.label)
|
||||||
|
.filter((e) => !!e)
|
||||||
|
.join(", "),
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>EnCoach</title>
|
<title>EnCoach</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.back()
|
router.back();
|
||||||
}}
|
}}
|
||||||
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
<BsChevronLeft />
|
>
|
||||||
</button>
|
<BsChevronLeft />
|
||||||
<h2 className="font-bold text-2xl">Student Performance ({students.length})</h2>
|
</button>
|
||||||
</div>
|
<h2 className="font-bold text-2xl">
|
||||||
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
Student Performance ({students.length})
|
||||||
</>
|
</h2>
|
||||||
</>
|
</div>
|
||||||
);
|
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StudentPerformance;
|
export default StudentPerformance;
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export const getAssignmentsByAssigner = async (id: string, startDate?: Date, end
|
|||||||
return await db.collection("assignments").find<Assignment>(query).toArray();
|
return await db.collection("assignments").find<Assignment>(query).toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssignments = async () => {
|
export const getAssignments = async (projection = {}) => {
|
||||||
return await db.collection("assignments").find<Assignment>({}).toArray();
|
return await db.collection("assignments").find<Assignment>({}, { projection }).toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssignment = async (id: string) => {
|
export const getAssignment = async (id: string) => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const db = client.db(process.env.MONGODB_DB);
|
|||||||
export const getEntityWithRoles = async (id: string): Promise<EntityWithRoles | undefined> => {
|
export const getEntityWithRoles = async (id: string): Promise<EntityWithRoles | undefined> => {
|
||||||
const entity = await getEntity(id);
|
const entity = await getEntity(id);
|
||||||
if (!entity) return undefined;
|
if (!entity) return undefined;
|
||||||
|
|
||||||
const roles = await getRolesByEntity(id);
|
const roles = await getRolesByEntity(id);
|
||||||
return { ...entity, roles };
|
return { ...entity, roles };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and } from "firebase/firestore";
|
|
||||||
import { groupBy, shuffle } from "lodash";
|
import { groupBy, shuffle } from "lodash";
|
||||||
import { CEFRLevels, Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam } from "@/interfaces/exam";
|
import { CEFRLevels, Exam, InstructorGender, SpeakingExam, Variant, WritingExam } from "@/interfaces/exam";
|
||||||
import { DeveloperUser, Stat, StudentUser, User } from "@/interfaces/user";
|
import { DeveloperUser, Stat, StudentUser, User } from "@/interfaces/user";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { getCorporateUser } from "@/resources/user";
|
import { Db } from "mongodb";
|
||||||
import { getUserCorporate } from "./groups.be";
|
|
||||||
import { Db, ObjectId } from "mongodb";
|
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import { MODULE_ARRAY } from "./moduleUtils";
|
import { MODULE_ARRAY } from "./moduleUtils";
|
||||||
import { mapBy } from ".";
|
import { mapBy } from ".";
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import client from "@/lib/mongodb";
|
|||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
export const getSessionsByUser = async (id: string, limit = 0, filter = {}) =>
|
export const getSessionsByUser = async (id: string, limit = 0, filter = {}, projection = {}) =>
|
||||||
await db
|
await db
|
||||||
.collection("sessions")
|
.collection("sessions")
|
||||||
.find<Session>({ user: id, ...filter })
|
.find<Session>({ user: id, ...filter }, { projection })
|
||||||
.limit(limit || 0)
|
.limit(limit || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
|
|||||||
@@ -129,19 +129,19 @@ export async function getUser(id: string, projection = {}): Promise<User | undef
|
|||||||
return !!user ? user : undefined;
|
return !!user ? user : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSpecificUsers(ids: string[]) {
|
export async function getSpecificUsers(ids: string[], projection = {}) {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ id: { $in: ids } }, { projection: { _id: 0 } })
|
.find<User>({ id: { $in: ids } }, { projection: { _id: 0, ...projection } })
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntityUsers(id: string, limit?: number, filter?: object) {
|
export async function getEntityUsers(id: string, limit?: number, filter?: object, projection = {}) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ "entities.id": id, ...(filter || {}) })
|
.find<User>({ "entities.id": id, ...(filter || {}) }, { projection: { _id: 0, ...projection } })
|
||||||
.limit(limit || 0)
|
.limit(limit || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ export async function getUserBalance(user: User) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
|
export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[], projection = {}) => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
["view_students"]: allowedStudentEntities,
|
["view_students"]: allowedStudentEntities,
|
||||||
@@ -244,12 +244,12 @@ export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]
|
|||||||
'view_corporates',
|
'view_corporates',
|
||||||
'view_mastercorporates',
|
'view_mastercorporates',
|
||||||
]);
|
]);
|
||||||
|
const [students, teachers, corporates, masterCorporates] = await Promise.all([
|
||||||
|
getEntitiesUsers(mapBy(allowedStudentEntities, 'id'), { type: "student" }, 0, projection),
|
||||||
const students = await getEntitiesUsers(mapBy(allowedStudentEntities, 'id'), { type: "student" })
|
getEntitiesUsers(mapBy(allowedTeacherEntities, 'id'), { type: "teacher" }, 0, projection),
|
||||||
const teachers = await getEntitiesUsers(mapBy(allowedTeacherEntities, 'id'), { type: "teacher" })
|
getEntitiesUsers(mapBy(allowedCorporateEntities, 'id'), { type: "corporate" }, 0, projection),
|
||||||
const corporates = await getEntitiesUsers(mapBy(allowedCorporateEntities, 'id'), { type: "corporate" })
|
getEntitiesUsers(mapBy(allowedMasterCorporateEntities, 'id'), { type: "mastercorporate" }, 0, projection),
|
||||||
const masterCorporates = await getEntitiesUsers(mapBy(allowedMasterCorporateEntities, 'id'), { type: "mastercorporate" })
|
])
|
||||||
|
|
||||||
return [...students, ...teachers, ...corporates, ...masterCorporates]
|
return [...students, ...teachers, ...corporates, ...masterCorporates]
|
||||||
}
|
}
|
||||||
@@ -266,11 +266,12 @@ export const countAllowedUsers = async (user: User, entities: EntityWithRoles[])
|
|||||||
'view_corporates',
|
'view_corporates',
|
||||||
'view_mastercorporates',
|
'view_mastercorporates',
|
||||||
]);
|
]);
|
||||||
|
const [student, teacher, corporate, mastercorporate] = await Promise.all([
|
||||||
const student = await countEntitiesUsers(mapBy(allowedStudentEntities, 'id'), { type: "student" })
|
countEntitiesUsers(mapBy(allowedStudentEntities, 'id'), { type: "student" }),
|
||||||
const teacher = await countEntitiesUsers(mapBy(allowedTeacherEntities, 'id'), { type: "teacher" })
|
countEntitiesUsers(mapBy(allowedTeacherEntities, 'id'), { type: "teacher" }),
|
||||||
const corporate = await countEntitiesUsers(mapBy(allowedCorporateEntities, 'id'), { type: "corporate" })
|
countEntitiesUsers(mapBy(allowedCorporateEntities, 'id'), { type: "corporate" }),
|
||||||
const mastercorporate = await countEntitiesUsers(mapBy(allowedMasterCorporateEntities, 'id'), { type: "mastercorporate" })
|
countEntitiesUsers(mapBy(allowedMasterCorporateEntities, 'id'), { type: "mastercorporate" }),
|
||||||
|
])
|
||||||
|
|
||||||
return { student, teacher, corporate, mastercorporate }
|
return { student, teacher, corporate, mastercorporate }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user