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,27 +14,38 @@ 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,
|
||||||
const hasBeenSubmitted = useMemo(() => assignment.results.map((r) => r.user).includes(user.id), [assignment.results, user.id])
|
session,
|
||||||
|
startAssignment,
|
||||||
|
resumeAssignment,
|
||||||
|
}: Props) {
|
||||||
|
const hasBeenSubmitted = useMemo(
|
||||||
|
() => assignment.results.map((r) => r.user).includes(user.id),
|
||||||
|
[assignment.results, user.id]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
||||||
assignment.results.map((r) => r.user).includes(user.id) && "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">
|
||||||
|
{assignment.name}
|
||||||
|
</h3>
|
||||||
<span className="flex justify-between gap-1 text-lg">
|
<span className="flex justify-between gap-1 text-lg">
|
||||||
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
@@ -45,7 +59,11 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
.map((e) => e.module)
|
.map((e) => e.module)
|
||||||
.sort(sortByModuleName)
|
.sort(sortByModuleName)
|
||||||
.map((module) => (
|
.map((module) => (
|
||||||
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
<ModuleBadge
|
||||||
|
className="scale-110 w-full"
|
||||||
|
key={module}
|
||||||
|
module={module}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
{futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
||||||
@@ -53,7 +71,8 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
color="rose"
|
color="rose"
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
disabled
|
disabled
|
||||||
variant="outline">
|
variant="outline"
|
||||||
|
>
|
||||||
Not yet started
|
Not yet started
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -61,7 +80,8 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
||||||
data-tip="Your screen size is too small to perform an assignment">
|
data-tip="Your screen size is too small to perform an assignment"
|
||||||
|
>
|
||||||
<Button className="h-full w-full !rounded-xl" variant="outline">
|
<Button className="h-full w-full !rounded-xl" variant="outline">
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
@@ -71,12 +91,14 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
data-tip="You have already started this assignment!"
|
data-tip="You have already started this assignment!"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
||||||
!!session && "tooltip",
|
!!session && "tooltip"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
className={clsx("w-full h-full !rounded-xl")}
|
className={clsx("w-full h-full !rounded-xl")}
|
||||||
onClick={() => startAssignment(assignment)}
|
onClick={() => startAssignment(assignment)}
|
||||||
variant="outline">
|
variant="outline"
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,12 +107,14 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"-md:hidden h-full w-full max-w-[50%] cursor-pointer"
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
className={clsx("w-full h-full !rounded-xl")}
|
className={clsx("w-full h-full !rounded-xl")}
|
||||||
onClick={() => resumeAssignment(session)}
|
onClick={() => resumeAssignment(session)}
|
||||||
color="green"
|
color="green"
|
||||||
variant="outline">
|
variant="outline"
|
||||||
|
>
|
||||||
Resume
|
Resume
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,11 +126,12 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
color="green"
|
color="green"
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
disabled
|
disabled
|
||||||
variant="outline">
|
variant="outline"
|
||||||
|
>
|
||||||
Submitted
|
Submitted
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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} />
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import clsx from "clsx";
|
|||||||
import { capitalize, uniqBy } from "lodash";
|
import { capitalize, uniqBy } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BsBook, BsBuilding, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
|
import {
|
||||||
|
BsBook,
|
||||||
|
BsBuilding,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsClipboard,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { futureAssignmentFilter } from "@/utils/assignments";
|
import { futureAssignmentFilter } from "@/utils/assignments";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
@@ -31,69 +39,140 @@ import { requestUser } from "@/utils/api";
|
|||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
||||||
|
|
||||||
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 (!checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"]))
|
if (
|
||||||
return redirect("/assignments")
|
!checkAccess(user, [
|
||||||
|
"admin",
|
||||||
|
"developer",
|
||||||
|
"corporate",
|
||||||
|
"teacher",
|
||||||
|
"mastercorporate",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return redirect("/assignments");
|
||||||
|
|
||||||
res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59");
|
res.setHeader(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, s-maxage=10, stale-while-revalidate=59"
|
||||||
|
);
|
||||||
|
|
||||||
const { id } = params as { id: string };
|
const { id } = params as { id: string };
|
||||||
|
|
||||||
const assignment = await getAssignment(id);
|
const assignment = await getAssignment(id);
|
||||||
if (!assignment) return redirect("/assignments")
|
if (!assignment) return redirect("/assignments");
|
||||||
|
|
||||||
const entity = await getEntityWithRoles(assignment.entity || "")
|
const entity = await getEntityWithRoles(assignment.entity || "");
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
const users = await getUsers()
|
const users = await getUsers(
|
||||||
|
{},
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
return { props: serialize({ user, users, assignment }) };
|
return { props: serialize({ user, users, assignment }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doesEntityAllow(user, entity, 'view_assignments')) return redirect("/assignments")
|
if (!doesEntityAllow(user, entity, "view_assignments"))
|
||||||
|
return redirect("/assignments");
|
||||||
|
|
||||||
const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntityUsers(entity.id));
|
const [users, gradingSystem] = await Promise.all([
|
||||||
const gradingSystem = await getGradingSystemByEntity(entity.id)
|
await (checkAccess(user, ["developer", "admin"])
|
||||||
|
? getUsers(
|
||||||
|
{},
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: getEntityUsers(
|
||||||
|
entity.id,
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
getGradingSystemByEntity(entity.id),
|
||||||
|
]);
|
||||||
|
|
||||||
return { props: serialize({ user, users, entity, assignment, gradingSystem }) };
|
return {
|
||||||
}, sessionOptions);
|
props: serialize({ user, users, entity, assignment, gradingSystem }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
entity?: EntityWithRoles
|
entity?: EntityWithRoles;
|
||||||
gradingSystem?: Grading
|
gradingSystem?: Grading;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssignmentView({ user, users, entity, assignment, gradingSystem }: Props) {
|
export default function AssignmentView({
|
||||||
const canDeleteAssignment = useEntityPermission(user, entity, 'delete_assignment')
|
user,
|
||||||
const canStartAssignment = useEntityPermission(user, entity, 'start_assignment')
|
users,
|
||||||
|
entity,
|
||||||
|
assignment,
|
||||||
|
gradingSystem,
|
||||||
|
}: Props) {
|
||||||
|
const canDeleteAssignment = useEntityPermission(
|
||||||
|
user,
|
||||||
|
entity,
|
||||||
|
"delete_assignment"
|
||||||
|
);
|
||||||
|
const canStartAssignment = useEntityPermission(
|
||||||
|
user,
|
||||||
|
entity,
|
||||||
|
"start_assignment"
|
||||||
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const dispatch = useExamStore((state) => state.dispatch);
|
const dispatch = useExamStore((state) => state.dispatch);
|
||||||
|
|
||||||
const deleteAssignment = async () => {
|
const deleteAssignment = async () => {
|
||||||
if (!canDeleteAssignment) return
|
if (!canDeleteAssignment) return;
|
||||||
if (!confirm("Are you sure you want to delete this assignment?")) return;
|
if (!confirm("Are you sure you want to delete this assignment?")) return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/assignments/${assignment?.id}`)
|
.delete(`/api/assignments/${assignment?.id}`)
|
||||||
.then(() => toast.success(`Successfully deleted the assignment "${assignment?.name}".`))
|
.then(() =>
|
||||||
|
toast.success(
|
||||||
|
`Successfully deleted the assignment "${assignment?.name}".`
|
||||||
|
)
|
||||||
|
)
|
||||||
.catch(() => toast.error("Something went wrong, please try again later."))
|
.catch(() => toast.error("Something went wrong, please try again later."))
|
||||||
.finally(() => router.push("/assignments"));
|
.finally(() => router.push("/assignments"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const startAssignment = () => {
|
const startAssignment = () => {
|
||||||
if (!canStartAssignment) return
|
if (!canStartAssignment) return;
|
||||||
if (!confirm("Are you sure you want to start this assignment?")) return;
|
if (!confirm("Are you sure you want to start this assignment?")) return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`/api/assignments/${assignment.id}/start`)
|
.post(`/api/assignments/${assignment.id}/start`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${assignment.name}" has been started successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${assignment.name}" has been started successfully!`
|
||||||
|
);
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -115,15 +194,26 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
const resultModuleBandScores = assignment.results.map((r) => {
|
const resultModuleBandScores = assignment.results.map((r) => {
|
||||||
const moduleStats = r.stats.filter((s) => s.module === module);
|
const moduleStats = r.stats.filter((s) => s.module === module);
|
||||||
|
|
||||||
const correct = moduleStats.reduce((acc, curr) => acc + curr.score.correct, 0);
|
const correct = moduleStats.reduce(
|
||||||
const total = moduleStats.reduce((acc, curr) => acc + curr.score.total, 0);
|
(acc, curr) => acc + curr.score.correct,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const total = moduleStats.reduce(
|
||||||
|
(acc, curr) => acc + curr.score.total,
|
||||||
|
0
|
||||||
|
);
|
||||||
return calculateBandScore(correct, total, module, r.type);
|
return calculateBandScore(correct, total, module, r.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / assignment.results.length;
|
return resultModuleBandScores.length === 0
|
||||||
|
? -1
|
||||||
|
: resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) /
|
||||||
|
assignment.results.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number; missing: number; correct: number }[] => {
|
const aggregateScoresByModule = (
|
||||||
|
stats: Stat[]
|
||||||
|
): { module: Module; total: number; missing: number; correct: number }[] => {
|
||||||
const scores: {
|
const scores: {
|
||||||
[key in Module]: { total: number; missing: number; correct: number };
|
[key in Module]: { total: number; missing: number; correct: number };
|
||||||
} = {
|
} = {
|
||||||
@@ -154,7 +244,9 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
stats.filter(x => !x.isPractice).forEach((x) => {
|
stats
|
||||||
|
.filter((x) => !x.isPractice)
|
||||||
|
.forEach((x) => {
|
||||||
scores[x.module!] = {
|
scores[x.module!] = {
|
||||||
total: scores[x.module!].total + x.score.total,
|
total: scores[x.module!].total + x.score.total,
|
||||||
correct: scores[x.module!].correct + x.score.correct,
|
correct: scores[x.module!].correct + x.score.correct,
|
||||||
@@ -167,28 +259,53 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
|
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const levelAverage = (aggregatedLevels: { module: Module, level: number }[]) =>
|
const levelAverage = (
|
||||||
aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length
|
aggregatedLevels: { module: Module; level: number }[]
|
||||||
|
) =>
|
||||||
|
aggregatedLevels.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.level,
|
||||||
|
0
|
||||||
|
) / aggregatedLevels.length;
|
||||||
|
|
||||||
const renderLevelScore = (stats: Stat[], aggregatedLevels: { module: Module, level: number }[]) => {
|
const renderLevelScore = (
|
||||||
const defaultLevelScore = levelAverage(aggregatedLevels).toFixed(1)
|
stats: Stat[],
|
||||||
if (!stats.every(s => s.module === "level")) return defaultLevelScore
|
aggregatedLevels: { module: Module; level: number }[]
|
||||||
if (!gradingSystem) return defaultLevelScore
|
) => {
|
||||||
|
const defaultLevelScore = levelAverage(aggregatedLevels).toFixed(1);
|
||||||
|
if (!stats.every((s) => s.module === "level")) return defaultLevelScore;
|
||||||
|
if (!gradingSystem) return defaultLevelScore;
|
||||||
|
|
||||||
const score = {
|
const score = {
|
||||||
correct: stats.reduce((acc, curr) => acc + curr.score.correct, 0),
|
correct: stats.reduce((acc, curr) => acc + curr.score.correct, 0),
|
||||||
total: stats.reduce((acc, curr) => acc + curr.score.total, 0)
|
total: stats.reduce((acc, curr) => acc + curr.score.total, 0),
|
||||||
}
|
};
|
||||||
|
|
||||||
const level: number = calculateBandScore(score.correct, score.total, "level", user.focus);
|
const level: number = calculateBandScore(
|
||||||
|
score.correct,
|
||||||
|
score.total,
|
||||||
|
"level",
|
||||||
|
user.focus
|
||||||
|
);
|
||||||
|
|
||||||
return getGradingLabel(level, gradingSystem.steps)
|
return getGradingLabel(level, gradingSystem.steps);
|
||||||
}
|
};
|
||||||
|
|
||||||
const customContent = (stats: Stat[], user: string, focus: "academic" | "general") => {
|
const customContent = (
|
||||||
const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0);
|
stats: Stat[],
|
||||||
const total = stats.reduce((accumulator, current) => accumulator + current.score.total, 0);
|
user: string,
|
||||||
const aggregatedScores = aggregateScoresByModule(stats).filter((x) => x.total > 0);
|
focus: "academic" | "general"
|
||||||
|
) => {
|
||||||
|
const correct = stats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.score.correct,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const total = stats.reduce(
|
||||||
|
(accumulator, current) => accumulator + current.score.total,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const aggregatedScores = aggregateScoresByModule(stats).filter(
|
||||||
|
(x) => x.total > 0
|
||||||
|
);
|
||||||
|
|
||||||
const aggregatedLevels = aggregatedScores.map((x) => ({
|
const aggregatedLevels = aggregatedScores.map((x) => ({
|
||||||
module: x.module,
|
module: x.module,
|
||||||
@@ -198,19 +315,22 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
const timeSpent = stats[0].timeSpent;
|
const timeSpent = stats[0].timeSpent;
|
||||||
|
|
||||||
const selectExam = () => {
|
const selectExam = () => {
|
||||||
const examPromises = uniqBy(stats, "exam").map((stat) => getExamById(stat.module, stat.exam));
|
const examPromises = uniqBy(stats, "exam").map((stat) =>
|
||||||
|
getExamById(stat.module, stat.exam)
|
||||||
|
);
|
||||||
|
|
||||||
Promise.all(examPromises).then((exams) => {
|
Promise.all(examPromises).then((exams) => {
|
||||||
if (exams.every((x) => !!x)) {
|
if (exams.every((x) => !!x)) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'INIT_SOLUTIONS', payload: {
|
type: "INIT_SOLUTIONS",
|
||||||
|
payload: {
|
||||||
exams: exams.map((x) => x!).sort(sortByModule),
|
exams: exams.map((x) => x!).sort(sortByModule),
|
||||||
modules: exams
|
modules: exams
|
||||||
.map((x) => x!)
|
.map((x) => x!)
|
||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map((x) => x!.module),
|
.map((x) => x!.module),
|
||||||
stats,
|
stats,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
router.push("/exam");
|
router.push("/exam");
|
||||||
}
|
}
|
||||||
@@ -221,11 +341,15 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
<>
|
<>
|
||||||
<div className="-md:items-center flex w-full justify-between 2xl:items-center">
|
<div className="-md:items-center flex w-full justify-between 2xl:items-center">
|
||||||
<div className="-md:gap-2 -md:items-center flex md:flex-col md:gap-1 2xl:flex-row 2xl:items-center 2xl:gap-2">
|
<div className="-md:gap-2 -md:items-center flex md:flex-col md:gap-1 2xl:flex-row 2xl:items-center 2xl:gap-2">
|
||||||
<span className="font-medium">{formatTimestamp(stats[0].date.toString())}</span>
|
<span className="font-medium">
|
||||||
|
{formatTimestamp(stats[0].date.toString())}
|
||||||
|
</span>
|
||||||
{timeSpent && (
|
{timeSpent && (
|
||||||
<>
|
<>
|
||||||
<span className="md:hidden 2xl:flex">• </span>
|
<span className="md:hidden 2xl:flex">• </span>
|
||||||
<span className="text-sm">{Math.floor(timeSpent / 60)} minutes</span>
|
<span className="text-sm">
|
||||||
|
{Math.floor(timeSpent / 60)} minutes
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -233,10 +357,10 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
correct / total >= 0.7 && "text-mti-purple",
|
correct / total >= 0.7 && "text-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
|
correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red",
|
||||||
correct / total < 0.3 && "text-mti-rose",
|
correct / total < 0.3 && "text-mti-rose"
|
||||||
)}>
|
)}
|
||||||
Level{' '}
|
>
|
||||||
{renderLevelScore(stats, aggregatedLevels)}
|
Level {renderLevelScore(stats, aggregatedLevels)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -251,8 +375,9 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
module === "listening" && "bg-ielts-listening",
|
module === "listening" && "bg-ielts-listening",
|
||||||
module === "writing" && "bg-ielts-writing",
|
module === "writing" && "bg-ielts-writing",
|
||||||
module === "speaking" && "bg-ielts-speaking",
|
module === "speaking" && "bg-ielts-speaking",
|
||||||
module === "level" && "bg-ielts-level",
|
module === "level" && "bg-ielts-level"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{module === "reading" && <BsBook className="h-4 w-4" />}
|
{module === "reading" && <BsBook className="h-4 w-4" />}
|
||||||
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
|
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
|
||||||
{module === "writing" && <BsPen className="h-4 w-4" />}
|
{module === "writing" && <BsPen className="h-4 w-4" />}
|
||||||
@@ -279,11 +404,14 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-gray-platinum -md:hidden flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out",
|
"border-mti-gray-platinum -md:hidden flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out",
|
||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 &&
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.7 &&
|
||||||
|
"hover:border-mti-red",
|
||||||
|
correct / total < 0.3 && "hover:border-mti-rose"
|
||||||
)}
|
)}
|
||||||
onClick={selectExam}
|
onClick={selectExam}
|
||||||
role="button">
|
role="button"
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -291,11 +419,14 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-gray-platinum -md:tooltip flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out md:hidden",
|
"border-mti-gray-platinum -md:tooltip flex cursor-pointer flex-col gap-4 rounded-xl border p-4 transition duration-300 ease-in-out md:hidden",
|
||||||
correct / total >= 0.7 && "hover:border-mti-purple",
|
correct / total >= 0.7 && "hover:border-mti-purple",
|
||||||
correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red",
|
correct / total >= 0.3 &&
|
||||||
correct / total < 0.3 && "hover:border-mti-rose",
|
correct / total < 0.7 &&
|
||||||
|
"hover:border-mti-red",
|
||||||
|
correct / total < 0.3 && "hover:border-mti-rose"
|
||||||
)}
|
)}
|
||||||
data-tip="Your screen size is too small to view previous exams."
|
data-tip="Your screen size is too small to view previous exams."
|
||||||
role="button">
|
role="button"
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,29 +444,46 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeInactiveAssignees = () => {
|
const removeInactiveAssignees = () => {
|
||||||
const mappedResults = mapBy(assignment.results, 'user')
|
const mappedResults = mapBy(assignment.results, "user");
|
||||||
const inactiveAssignees = assignment.assignees.filter((a) => !mappedResults.includes(a))
|
const inactiveAssignees = assignment.assignees.filter(
|
||||||
const activeAssignees = assignment.assignees.filter((a) => mappedResults.includes(a))
|
(a) => !mappedResults.includes(a)
|
||||||
|
);
|
||||||
|
const activeAssignees = assignment.assignees.filter((a) =>
|
||||||
|
mappedResults.includes(a)
|
||||||
|
);
|
||||||
|
|
||||||
if (!confirm(`Are you sure you want to remove ${inactiveAssignees.length} assignees?`)) return
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to remove ${inactiveAssignees.length} assignees?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/assignments/${assignment.id}`, { assignees: activeAssignees })
|
.patch(`/api/assignments/${assignment.id}`, {
|
||||||
|
assignees: activeAssignees,
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${assignment.name}" has been updated successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${assignment.name}" has been updated successfully!`
|
||||||
|
);
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
toast.error("Something went wrong, please try again later!");
|
toast.error("Something went wrong, please try again later!");
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
const origin = window.location.origin
|
const origin = window.location.origin;
|
||||||
await navigator.clipboard.writeText(`${origin}/exam?assignment=${assignment.id}`)
|
await navigator.clipboard.writeText(
|
||||||
toast.success("The URL to the assignment has been copied to your clipboard!")
|
`${origin}/exam?assignment=${assignment.id}`
|
||||||
}
|
);
|
||||||
|
toast.success(
|
||||||
|
"The URL to the assignment has been copied to your clipboard!"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -352,7 +500,10 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-full flex items-center justify-between">
|
<div className="w-full flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link
|
||||||
|
href="/assignments"
|
||||||
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
|
>
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">{assignment.name}</h2>
|
<h2 className="font-bold text-2xl">{assignment.name}</h2>
|
||||||
@@ -371,16 +522,28 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`}
|
label={`${assignment?.results.length}/${assignment?.assignees.length} assignees completed`}
|
||||||
className="h-6"
|
className="h-6"
|
||||||
textClassName={
|
textClassName={
|
||||||
(assignment?.results.length || 0) / (assignment?.assignees.length || 1) < 0.5
|
(assignment?.results.length || 0) /
|
||||||
|
(assignment?.assignees.length || 1) <
|
||||||
|
0.5
|
||||||
? "!text-mti-gray-dim font-light"
|
? "!text-mti-gray-dim font-light"
|
||||||
: "text-white"
|
: "text-white"
|
||||||
}
|
}
|
||||||
percentage={((assignment?.results.length || 0) / (assignment?.assignees.length || 1)) * 100}
|
percentage={
|
||||||
|
((assignment?.results.length || 0) /
|
||||||
|
(assignment?.assignees.length || 1)) *
|
||||||
|
100
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-start gap-8">
|
<div className="flex items-start gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span>Start Date: {moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>
|
||||||
<span>End Date: {moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}</span>
|
Start Date:{" "}
|
||||||
|
{moment(assignment?.startDate).format("DD/MM/YY, HH:mm")}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
End Date:{" "}
|
||||||
|
{moment(assignment?.endDate).format("DD/MM/YY, HH:mm")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span>
|
<span>
|
||||||
@@ -390,12 +553,18 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
.map((u) => `${u.name} (${u.email})`)
|
.map((u) => `${u.name} (${u.email})`)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
<span>Assigner: {getUserName(users.find((x) => x.id === assignment?.assigner))}</span>
|
<span>
|
||||||
|
Assigner:{" "}
|
||||||
|
{getUserName(users.find((x) => x.id === assignment?.assigner))}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{assignment.assignees.length !== 0 && assignment.results.length !== assignment.assignees.length && (
|
{assignment.assignees.length !== 0 &&
|
||||||
<Button onClick={removeInactiveAssignees} variant="outline">Remove Inactive Assignees</Button>
|
assignment.results.length !== assignment.assignees.length && (
|
||||||
|
<Button onClick={removeInactiveAssignees} variant="outline">
|
||||||
|
Remove Inactive Assignees
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -412,15 +581,22 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
module === "listening" && "bg-ielts-listening",
|
module === "listening" && "bg-ielts-listening",
|
||||||
module === "writing" && "bg-ielts-writing",
|
module === "writing" && "bg-ielts-writing",
|
||||||
module === "speaking" && "bg-ielts-speaking",
|
module === "speaking" && "bg-ielts-speaking",
|
||||||
module === "level" && "bg-ielts-level",
|
module === "level" && "bg-ielts-level"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{module === "reading" && <BsBook className="h-4 w-4" />}
|
{module === "reading" && <BsBook className="h-4 w-4" />}
|
||||||
{module === "listening" && <BsHeadphones className="h-4 w-4" />}
|
{module === "listening" && (
|
||||||
|
<BsHeadphones className="h-4 w-4" />
|
||||||
|
)}
|
||||||
{module === "writing" && <BsPen className="h-4 w-4" />}
|
{module === "writing" && <BsPen className="h-4 w-4" />}
|
||||||
{module === "speaking" && <BsMegaphone className="h-4 w-4" />}
|
{module === "speaking" && (
|
||||||
|
<BsMegaphone className="h-4 w-4" />
|
||||||
|
)}
|
||||||
{module === "level" && <BsClipboard className="h-4 w-4" />}
|
{module === "level" && <BsClipboard className="h-4 w-4" />}
|
||||||
{calculateAverageModuleScore(module) > -1 && (
|
{calculateAverageModuleScore(module) > -1 && (
|
||||||
<span className="text-sm">{calculateAverageModuleScore(module).toFixed(1)}</span>
|
<span className="text-sm">
|
||||||
|
{calculateAverageModuleScore(module).toFixed(1)}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -428,35 +604,59 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-xl font-bold">
|
<span className="text-xl font-bold">
|
||||||
Results ({assignment?.results.length}/{assignment?.assignees.length})
|
Results ({assignment?.results.length}/
|
||||||
|
{assignment?.assignees.length})
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
{assignment && assignment?.results.length > 0 && (
|
{assignment && assignment?.results.length > 0 && (
|
||||||
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 xl:gap-6">
|
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 xl:gap-6">
|
||||||
{assignment.results.map((r) => customContent(r.stats, r.user, r.type))}
|
{assignment.results.map((r) =>
|
||||||
|
customContent(r.stats, r.user, r.type)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{assignment && assignment?.results.length === 0 && <span className="ml-1 font-semibold">No results yet...</span>}
|
{assignment && assignment?.results.length === 0 && (
|
||||||
|
<span className="ml-1 font-semibold">No results yet...</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4 w-full items-center justify-end">
|
<div className="flex gap-4 w-full items-center justify-end">
|
||||||
<Button variant="outline" color="purple" className="w-full max-w-[200px]" onClick={copyLink}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="purple"
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
onClick={copyLink}
|
||||||
|
>
|
||||||
Copy Link
|
Copy Link
|
||||||
</Button>
|
</Button>
|
||||||
{assignment &&
|
{assignment &&
|
||||||
(assignment.results.length === assignment.assignees.length || moment().isAfter(moment(assignment.endDate))) && (
|
(assignment.results.length === assignment.assignees.length ||
|
||||||
<Button variant="outline" color="red" className="w-full max-w-[200px]" onClick={deleteAssignment}>
|
moment().isAfter(moment(assignment.endDate))) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
onClick={deleteAssignment}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/** if the assignment is not deemed as active yet, display start */}
|
{/** if the assignment is not deemed as active yet, display start */}
|
||||||
{shouldRenderStart() && (
|
{shouldRenderStart() && (
|
||||||
<Button variant="outline" color="green" className="w-full max-w-[200px]" onClick={startAssignment}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="green"
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
onClick={startAssignment}
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => router.push("/assignments")} className="w-full max-w-[200px]">
|
<Button
|
||||||
|
onClick={() => router.push("/assignments")}
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import { requestUser } from "@/utils/api";
|
|||||||
import { getAssignment } from "@/utils/assignments.be";
|
import { getAssignment } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
||||||
import { checkAccess, doesEntityAllow, findAllowedEntities } from "@/utils/permissions";
|
import {
|
||||||
|
checkAccess,
|
||||||
|
doesEntityAllow,
|
||||||
|
findAllowedEntities,
|
||||||
|
} from "@/utils/permissions";
|
||||||
import { calculateAverageLevel } from "@/utils/score";
|
import { calculateAverageLevel } from "@/utils/score";
|
||||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -32,34 +36,91 @@ import { useRouter } from "next/router";
|
|||||||
import { generate } from "random-words";
|
import { generate } from "random-words";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
|
import {
|
||||||
|
BsBook,
|
||||||
|
BsCheckCircle,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsClipboard,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
BsXCircle,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } 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");
|
||||||
|
|
||||||
res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59");
|
res.setHeader(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, s-maxage=10, stale-while-revalidate=59"
|
||||||
|
);
|
||||||
|
|
||||||
const { id } = params as { id: string };
|
const { id } = params as { id: string };
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
|
const isAdmin = checkAccess(user, ["developer", "admin"]);
|
||||||
|
|
||||||
const assignment = await getAssignment(id);
|
const [assignment, entities] = await Promise.all([
|
||||||
if (!assignment) return redirect("/assignments")
|
getAssignment(id),
|
||||||
|
getEntitiesWithRoles(isAdmin ? undefined : entityIDS),
|
||||||
|
]);
|
||||||
|
|
||||||
const entities = await (checkAccess(user, ["developer", "admin"]) ? getEntitiesWithRoles() : getEntitiesWithRoles(entityIDS));
|
if (!assignment) return redirect("/assignments");
|
||||||
const entity = entities.find((e) => e.id === assignment.entity)
|
const entity = entities.find((e) => e.id === assignment.entity);
|
||||||
|
|
||||||
if (!entity) return redirect("/assignments")
|
if (!entity) return redirect("/assignments");
|
||||||
if (!doesEntityAllow(user, entity, 'edit_assignment')) return redirect("/assignments")
|
|
||||||
|
|
||||||
const allowedEntities = findAllowedEntities(user, entities, 'edit_assignment')
|
if (!doesEntityAllow(user, entity, "edit_assignment"))
|
||||||
|
return redirect("/assignments");
|
||||||
|
|
||||||
const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
|
const allowedEntities = findAllowedEntities(
|
||||||
const groups = await (checkAccess(user, ["developer", "admin"]) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id')));
|
user,
|
||||||
|
entities,
|
||||||
|
"edit_assignment"
|
||||||
|
);
|
||||||
|
|
||||||
return { props: serialize({ user, users, entities: allowedEntities, assignment, groups }) };
|
const allowEntitiesIDs = mapBy(allowedEntities, "id");
|
||||||
}, sessionOptions);
|
|
||||||
|
const [users, groups] = await Promise.all([
|
||||||
|
isAdmin
|
||||||
|
? getUsers(
|
||||||
|
{},
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
type: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
levels: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: getEntitiesUsers(allowEntitiesIDs, {}, 0, {
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
type: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
levels: 1,
|
||||||
|
}),
|
||||||
|
isAdmin ? getGroups() : getGroupsByEntities(allowEntitiesIDs),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
users,
|
||||||
|
entities: allowedEntities,
|
||||||
|
assignment,
|
||||||
|
groups,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
@@ -71,24 +132,44 @@ interface Props {
|
|||||||
|
|
||||||
const SIZE = 9;
|
const SIZE = 9;
|
||||||
|
|
||||||
export default function AssignmentsPage({ assignment, user, users, entities, groups }: Props) {
|
export default function AssignmentsPage({
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>(assignment.exams.map((e) => e.module));
|
assignment,
|
||||||
|
user,
|
||||||
|
users,
|
||||||
|
entities,
|
||||||
|
groups,
|
||||||
|
}: Props) {
|
||||||
|
const [selectedModules, setSelectedModules] = useState<Module[]>(
|
||||||
|
assignment.exams.map((e) => e.module)
|
||||||
|
);
|
||||||
const [assignees, setAssignees] = useState<string[]>(assignment.assignees);
|
const [assignees, setAssignees] = useState<string[]>(assignment.assignees);
|
||||||
const [teachers, setTeachers] = useState<string[]>(assignment.teachers || []);
|
const [teachers, setTeachers] = useState<string[]>(assignment.teachers || []);
|
||||||
const [entity, setEntity] = useState<string | undefined>(assignment.entity || entities[0]?.id);
|
const [entity, setEntity] = useState<string | undefined>(
|
||||||
|
assignment.entity || entities[0]?.id
|
||||||
|
);
|
||||||
const [name, setName] = useState(assignment.name);
|
const [name, setName] = useState(assignment.name);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment(assignment.startDate).toDate());
|
const [startDate, setStartDate] = useState<Date | null>(
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment(assignment.endDate).toDate());
|
moment(assignment.startDate).toDate()
|
||||||
|
);
|
||||||
|
const [endDate, setEndDate] = useState<Date | null>(
|
||||||
|
moment(assignment.endDate).toDate()
|
||||||
|
);
|
||||||
|
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
const [instructorGender, setInstructorGender] = useState<InstructorGender>(assignment?.instructorGender || "varied");
|
const [instructorGender, setInstructorGender] = useState<InstructorGender>(
|
||||||
|
assignment?.instructorGender || "varied"
|
||||||
|
);
|
||||||
|
|
||||||
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
||||||
const [released, setReleased] = useState<boolean>(assignment.released || false);
|
const [released, setReleased] = useState<boolean>(
|
||||||
|
assignment.released || false
|
||||||
|
);
|
||||||
|
|
||||||
const [autoStart, setAutostart] = useState<boolean>(assignment.autoStart || false);
|
const [autoStart, setAutostart] = useState<boolean>(
|
||||||
|
assignment.autoStart || false
|
||||||
|
);
|
||||||
|
|
||||||
const [useRandomExams, setUseRandomExams] = useState(true);
|
const [useRandomExams, setUseRandomExams] = useState(true);
|
||||||
const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]);
|
const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]);
|
||||||
@@ -96,19 +177,34 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
const { exams } = useExams();
|
const { exams } = useExams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const classrooms = useMemo(() => groups.filter((e) => e.entity === entity), [entity, groups]);
|
const classrooms = useMemo(
|
||||||
|
() => groups.filter((e) => e.entity === entity),
|
||||||
|
[entity, groups]
|
||||||
|
);
|
||||||
|
|
||||||
const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]);
|
const userStudents = useMemo(
|
||||||
const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]);
|
() => users.filter((x) => x.type === "student"),
|
||||||
|
[users]
|
||||||
|
);
|
||||||
|
const userTeachers = useMemo(
|
||||||
|
() => users.filter((x) => x.type === "teacher"),
|
||||||
|
[users]
|
||||||
|
);
|
||||||
|
|
||||||
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents);
|
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } =
|
||||||
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers);
|
useListSearch([["name"], ["email"]], userStudents);
|
||||||
|
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } =
|
||||||
|
useListSearch([["name"], ["email"]], userTeachers);
|
||||||
|
|
||||||
const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE);
|
const { items: studentRows, renderMinimal: renderStudentPagination } =
|
||||||
const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE);
|
usePagination(filteredStudentsRows, SIZE);
|
||||||
|
const { items: teacherRows, renderMinimal: renderTeacherPagination } =
|
||||||
|
usePagination(filteredTeachersRows, SIZE);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
|
setExamIDs((prev) =>
|
||||||
|
prev.filter((x) => selectedModules.includes(x.module))
|
||||||
|
);
|
||||||
}, [selectedModules]);
|
}, [selectedModules]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -118,21 +214,33 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
setSelectedModules((prev) =>
|
||||||
|
prev.includes(module) ? modules : [...modules, module]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAssignee = (user: User) => {
|
const toggleAssignee = (user: User) => {
|
||||||
setAssignees((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
|
setAssignees((prev) =>
|
||||||
|
prev.includes(user.id)
|
||||||
|
? prev.filter((a) => a !== user.id)
|
||||||
|
: [...prev, user.id]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTeacher = (user: User) => {
|
const toggleTeacher = (user: User) => {
|
||||||
setTeachers((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
|
setTeachers((prev) =>
|
||||||
|
prev.includes(user.id)
|
||||||
|
? prev.filter((a) => a !== user.id)
|
||||||
|
: [...prev, user.id]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAssignment = () => {
|
const createAssignment = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
(assignment ? axios.patch : axios.post)(`/api/assignments/${assignment.id}`, {
|
(assignment ? axios.patch : axios.post)(
|
||||||
|
`/api/assignments/${assignment.id}`,
|
||||||
|
{
|
||||||
assignees,
|
assignees,
|
||||||
name,
|
name,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -146,9 +254,12 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
instructorGender,
|
instructorGender,
|
||||||
released,
|
released,
|
||||||
autoStart,
|
autoStart,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been updated successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been updated successfully!`
|
||||||
|
);
|
||||||
router.push(`/assignments/${assignment.id}`);
|
router.push(`/assignments/${assignment.id}`);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -159,14 +270,21 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteAssignment = () => {
|
const deleteAssignment = () => {
|
||||||
if (!confirm(`Are you sure you want to delete the "${assignment.name}" assignment?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to delete the "${assignment.name}" assignment?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
console.log("GOT HERE");
|
console.log("GOT HERE");
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/assignments/${assignment.id}`)
|
.delete(`/api/assignments/${assignment.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been deleted successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been deleted successfully!`
|
||||||
|
);
|
||||||
router.push("/assignments");
|
router.push("/assignments");
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -183,7 +301,9 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
axios
|
axios
|
||||||
.post(`/api/assignments/${assignment.id}/start`)
|
.post(`/api/assignments/${assignment.id}/start`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(`The assignment "${name}" has been started successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been started successfully!`
|
||||||
|
);
|
||||||
router.push(`/assignments/${assignment.id}`);
|
router.push(`/assignments/${assignment.id}`);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -195,10 +315,14 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
const origin = window.location.origin
|
const origin = window.location.origin;
|
||||||
await navigator.clipboard.writeText(`${origin}/exam?assignment=${assignment.id}`)
|
await navigator.clipboard.writeText(
|
||||||
toast.success("The URL to the assignment has been copied to your clipboard!")
|
`${origin}/exam?assignment=${assignment.id}`
|
||||||
}
|
);
|
||||||
|
toast.success(
|
||||||
|
"The URL to the assignment has been copied to your clipboard!"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -214,7 +338,10 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
<>
|
<>
|
||||||
<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="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link
|
||||||
|
href="/assignments"
|
||||||
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
|
>
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">Edit {assignment.name}</h2>
|
<h2 className="font-bold text-2xl">Edit {assignment.name}</h2>
|
||||||
@@ -224,109 +351,180 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
<section className="w-full grid -md:grid-cols-1 md:grid-cols-3 place-items-center -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
<section className="w-full grid -md:grid-cols-1 md:grid-cols-3 place-items-center -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("reading")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("reading") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("reading")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsBook className="text-white w-7 h-7" />
|
<BsBook className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Reading</span>
|
<span className="ml-8 font-semibold">Reading</span>
|
||||||
{!selectedModules.includes("reading") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("reading") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("reading") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("reading") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("listening")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("listening") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("listening")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsHeadphones className="text-white w-7 h-7" />
|
<BsHeadphones className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Listening</span>
|
<span className="ml-8 font-semibold">Listening</span>
|
||||||
{!selectedModules.includes("listening") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("listening") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("listening") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("listening") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={
|
||||||
(!selectedModules.includes("level") && selectedModules.length === 0) || selectedModules.includes("level")
|
(!selectedModules.includes("level") &&
|
||||||
|
selectedModules.length === 0) ||
|
||||||
|
selectedModules.includes("level")
|
||||||
? () => toggleModule("level")
|
? () => toggleModule("level")
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("level") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("level")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsClipboard className="text-white w-7 h-7" />
|
<BsClipboard className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Level</span>
|
<span className="ml-8 font-semibold">Level</span>
|
||||||
{!selectedModules.includes("level") && selectedModules.length === 0 && (
|
{!selectedModules.includes("level") &&
|
||||||
|
selectedModules.length === 0 && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{!selectedModules.includes("level") && selectedModules.length > 0 && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{!selectedModules.includes("level") &&
|
||||||
{selectedModules.includes("level") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
selectedModules.length > 0 && (
|
||||||
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("level") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("writing")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("writing") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("writing")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsPen className="text-white w-7 h-7" />
|
<BsPen className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Writing</span>
|
<span className="ml-8 font-semibold">Writing</span>
|
||||||
{!selectedModules.includes("writing") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("writing") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("writing") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("writing") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("speaking")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("speaking") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("speaking")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsMegaphone className="text-white w-7 h-7" />
|
<BsMegaphone className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Speaking</span>
|
<span className="ml-8 font-semibold">Speaking</span>
|
||||||
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("speaking") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("speaking") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("speaking") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
onChange={(e) => setName(e)}
|
||||||
|
defaultValue={name}
|
||||||
|
label="Assignment Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Entity"
|
label="Entity"
|
||||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||||
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
||||||
defaultValue={{ value: entities[0]?.id, label: entities[0]?.label }}
|
defaultValue={{
|
||||||
|
value: entities[0]?.id,
|
||||||
|
label: entities[0]?.label,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Limit Start Date *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Limit Start Date *
|
||||||
|
</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip z-10",
|
"hover:border-mti-purple tooltip z-10",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
popperClassName="!z-20"
|
popperClassName="!z-20"
|
||||||
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
|
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
|
||||||
@@ -337,12 +535,14 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">End Date *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
End Date *
|
||||||
|
</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip z-10",
|
"hover:border-mti-purple tooltip z-10",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
popperClassName="!z-20"
|
popperClassName="!z-20"
|
||||||
filterTime={(date) => moment(date).isAfter(startDate)}
|
filterTime={(date) => moment(date).isAfter(startDate)}
|
||||||
@@ -356,13 +556,19 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
|
|
||||||
{selectedModules.includes("speaking") && (
|
{selectedModules.includes("speaking") && (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Speaking Instructor's Gender
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: instructorGender,
|
value: instructorGender,
|
||||||
label: capitalize(instructorGender),
|
label: capitalize(instructorGender),
|
||||||
}}
|
}}
|
||||||
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
|
onChange={(value) =>
|
||||||
|
value
|
||||||
|
? setInstructorGender(value.value as InstructorGender)
|
||||||
|
: null
|
||||||
|
}
|
||||||
disabled={!selectedModules.includes("speaking") || !!assignment}
|
disabled={!selectedModules.includes("speaking") || !!assignment}
|
||||||
options={[
|
options={[
|
||||||
{ value: "male", label: "Male" },
|
{ value: "male", label: "Male" },
|
||||||
@@ -382,11 +588,16 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
<div className="grid md:grid-cols-2 w-full gap-4">
|
<div className="grid md:grid-cols-2 w-full gap-4">
|
||||||
{selectedModules.map((module) => (
|
{selectedModules.map((module) => (
|
||||||
<div key={module} className="flex flex-col gap-3 w-full">
|
<div key={module} className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">{capitalize(module)} Exam</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
{capitalize(module)} Exam
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: examIDs.find((e) => e.module === module)?.id || null,
|
value:
|
||||||
label: examIDs.find((e) => e.module === module)?.id || "",
|
examIDs.find((e) => e.module === module)?.id ||
|
||||||
|
null,
|
||||||
|
label:
|
||||||
|
examIDs.find((e) => e.module === module)?.id || "",
|
||||||
}}
|
}}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value
|
value
|
||||||
@@ -394,7 +605,9 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
...prev.filter((x) => x.module !== module),
|
...prev.filter((x) => x.module !== module),
|
||||||
{ id: value.value!, module },
|
{ id: value.value!, module },
|
||||||
])
|
])
|
||||||
: setExamIDs((prev) => prev.filter((x) => x.module !== module))
|
: setExamIDs((prev) =>
|
||||||
|
prev.filter((x) => x.module !== module)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
options={exams
|
options={exams
|
||||||
.filter((x) => !x.isDiagnostic && x.module === module)
|
.filter((x) => !x.isDiagnostic && x.module === module)
|
||||||
@@ -408,25 +621,40 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="w-full flex flex-col gap-4">
|
<section className="w-full flex flex-col gap-4">
|
||||||
<span className="font-semibold">Assignees ({assignees.length} selected)</span>
|
<span className="font-semibold">
|
||||||
|
Assignees ({assignees.length} selected)
|
||||||
|
</span>
|
||||||
<div className="grid grid-cols-5 gap-4">
|
<div className="grid grid-cols-5 gap-4">
|
||||||
{classrooms.map((g) => (
|
{classrooms.map((g) => (
|
||||||
<button
|
<button
|
||||||
key={g.id}
|
key={g.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
|
const groupStudentIds = users.reduce<string[]>((acc, u) => {
|
||||||
|
if (g.participants.includes(u.id)) {
|
||||||
|
acc.push(u.id);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
if (groupStudentIds.every((u) => assignees.includes(u))) {
|
if (groupStudentIds.every((u) => assignees.includes(u))) {
|
||||||
setAssignees((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
|
setAssignees((prev) =>
|
||||||
|
prev.filter((a) => !groupStudentIds.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setAssignees((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
|
setAssignees((prev) => [
|
||||||
|
...prev.filter((a) => !groupStudentIds.includes(a)),
|
||||||
|
...groupStudentIds,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
|
users
|
||||||
"!bg-mti-purple-light !text-white",
|
.filter((u) => g.participants.includes(u.id))
|
||||||
)}>
|
.every((u) => assignees.includes(u.id)) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{g.name}
|
{g.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -444,9 +672,12 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
assignees.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
|
assignees.includes(user.id)
|
||||||
|
? "border-mti-purple"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
)}
|
)}
|
||||||
key={user.id}>
|
key={user.id}
|
||||||
|
>
|
||||||
<span className="flex flex-col gap-0 justify-center">
|
<span className="flex flex-col gap-0 justify-center">
|
||||||
<span className="font-semibold">{user.name}</span>
|
<span className="font-semibold">{user.name}</span>
|
||||||
<span className="text-sm opacity-80">{user.email}</span>
|
<span className="text-sm opacity-80">{user.email}</span>
|
||||||
@@ -472,25 +703,43 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
|
|
||||||
{user.type !== "teacher" && (
|
{user.type !== "teacher" && (
|
||||||
<section className="w-full flex flex-col gap-3">
|
<section className="w-full flex flex-col gap-3">
|
||||||
<span className="font-semibold">Teachers ({teachers.length} selected)</span>
|
<span className="font-semibold">
|
||||||
|
Teachers ({teachers.length} selected)
|
||||||
|
</span>
|
||||||
<div className="grid grid-cols-5 gap-4">
|
<div className="grid grid-cols-5 gap-4">
|
||||||
{classrooms.map((g) => (
|
{classrooms.map((g) => (
|
||||||
<button
|
<button
|
||||||
key={g.id}
|
key={g.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
|
const groupStudentIds = users.reduce<string[]>(
|
||||||
|
(acc, u) => {
|
||||||
|
if (g.participants.includes(u.id)) {
|
||||||
|
acc.push(u.id);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
if (groupStudentIds.every((u) => teachers.includes(u))) {
|
if (groupStudentIds.every((u) => teachers.includes(u))) {
|
||||||
setTeachers((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
|
setTeachers((prev) =>
|
||||||
|
prev.filter((a) => !groupStudentIds.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setTeachers((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
|
setTeachers((prev) => [
|
||||||
|
...prev.filter((a) => !groupStudentIds.includes(a)),
|
||||||
|
...groupStudentIds,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
|
users
|
||||||
"!bg-mti-purple-light !text-white",
|
.filter((u) => g.participants.includes(u.id))
|
||||||
)}>
|
.every((u) => teachers.includes(u.id)) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{g.name}
|
{g.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -508,9 +757,12 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
teachers.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
|
teachers.includes(user.id)
|
||||||
|
? "border-mti-purple"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
)}
|
)}
|
||||||
key={user.id}>
|
key={user.id}
|
||||||
|
>
|
||||||
<span className="flex flex-col gap-0 justify-center">
|
<span className="flex flex-col gap-0 justify-center">
|
||||||
<span className="font-semibold">{user.name}</span>
|
<span className="font-semibold">{user.name}</span>
|
||||||
<span className="text-sm opacity-80">{user.email}</span>
|
<span className="text-sm opacity-80">{user.email}</span>
|
||||||
@@ -529,21 +781,40 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4 w-full items-end">
|
<div className="flex gap-4 w-full items-end">
|
||||||
<Checkbox isChecked={variant === "full"} onChange={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
<Checkbox
|
||||||
|
isChecked={variant === "full"}
|
||||||
|
onChange={() =>
|
||||||
|
setVariant((prev) => (prev === "full" ? "partial" : "full"))
|
||||||
|
}
|
||||||
|
>
|
||||||
Full length exams
|
Full length exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={generateMultiple}
|
||||||
|
onChange={() => setGenerateMultiple((d) => !d)}
|
||||||
|
>
|
||||||
Generate different exams
|
Generate different exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={released} onChange={() => setReleased((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={released}
|
||||||
|
onChange={() => setReleased((d) => !d)}
|
||||||
|
>
|
||||||
Auto release results
|
Auto release results
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={autoStart} onChange={() => setAutostart((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={autoStart}
|
||||||
|
onChange={() => setAutostart((d) => !d)}
|
||||||
|
>
|
||||||
Auto start exam
|
Auto start exam
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 w-full justify-end">
|
<div className="flex gap-4 w-full justify-end">
|
||||||
<Button variant="outline" color="purple" className="w-full max-w-[200px]" onClick={copyLink}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="purple"
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
onClick={copyLink}
|
||||||
|
>
|
||||||
Copy Link
|
Copy Link
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -551,7 +822,8 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => router.push("/assignments")}
|
onClick={() => router.push("/assignments")}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -560,7 +832,8 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={startAssignment}
|
onClick={startAssignment}
|
||||||
disabled={isLoading || moment().isAfter(startDate)}
|
disabled={isLoading || moment().isAfter(startDate)}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -569,7 +842,8 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={deleteAssignment}
|
onClick={deleteAssignment}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -583,7 +857,8 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
}
|
}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]"
|
||||||
onClick={createAssignment}
|
onClick={createAssignment}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,21 +32,61 @@ import { useRouter } from "next/router";
|
|||||||
import { generate } from "random-words";
|
import { generate } from "random-words";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
|
import {
|
||||||
|
BsBook,
|
||||||
|
BsCheckCircle,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsClipboard,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
BsXCircle,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
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 (checkAccess(user, ["developer", "admin"]) ? getEntitiesWithRoles() : getEntitiesWithRoles(entityIDS));
|
const entities = await (checkAccess(user, ["developer", "admin"])
|
||||||
|
? getEntitiesWithRoles()
|
||||||
|
: getEntitiesWithRoles(entityIDS));
|
||||||
|
|
||||||
const allowedEntities = findAllowedEntities(user, entities, 'create_assignment')
|
const allowedEntities = findAllowedEntities(
|
||||||
if (allowedEntities.length === 0) return redirect("/assignments")
|
user,
|
||||||
|
entities,
|
||||||
|
"create_assignment"
|
||||||
|
);
|
||||||
|
if (allowedEntities.length === 0) return redirect("/assignments");
|
||||||
|
|
||||||
const users = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
|
const [users, groups] = await Promise.all([
|
||||||
const groups = await (isAdmin(user) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id')));
|
isAdmin(user)
|
||||||
|
? getUsers(
|
||||||
|
{},
|
||||||
|
0,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
type: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
levels: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: getEntitiesUsers(mapBy(allowedEntities, "id"), {}, 0, {
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
type: 1,
|
||||||
|
name: 1,
|
||||||
|
email: 1,
|
||||||
|
levels: 1,
|
||||||
|
}),
|
||||||
|
isAdmin(user)
|
||||||
|
? getGroups()
|
||||||
|
: getGroupsByEntities(mapBy(allowedEntities, "id")),
|
||||||
|
]);
|
||||||
|
|
||||||
return { props: serialize({ user, users, entities, groups }) };
|
return { props: serialize({ user, users, entities, groups }) };
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
@@ -61,10 +101,17 @@ interface Props {
|
|||||||
|
|
||||||
const SIZE = 9;
|
const SIZE = 9;
|
||||||
|
|
||||||
export default function AssignmentsPage({ user, users, groups, entities }: Props) {
|
export default function AssignmentsPage({
|
||||||
|
user,
|
||||||
|
users,
|
||||||
|
groups,
|
||||||
|
entities,
|
||||||
|
}: Props) {
|
||||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
const [assignees, setAssignees] = useState<string[]>([]);
|
const [assignees, setAssignees] = useState<string[]>([]);
|
||||||
const [teachers, setTeachers] = useState<string[]>([...(user.type === "teacher" ? [user.id] : [])]);
|
const [teachers, setTeachers] = useState<string[]>([
|
||||||
|
...(user.type === "teacher" ? [user.id] : []),
|
||||||
|
]);
|
||||||
const [entity, setEntity] = useState<string | undefined>(entities[0]?.id);
|
const [entity, setEntity] = useState<string | undefined>(entities[0]?.id);
|
||||||
const [name, setName] = useState(
|
const [name, setName] = useState(
|
||||||
generate({
|
generate({
|
||||||
@@ -74,14 +121,19 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
max: 3,
|
max: 3,
|
||||||
join: " ",
|
join: " ",
|
||||||
formatter: capitalize,
|
formatter: capitalize,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [startDate, setStartDate] = useState<Date | null>(moment().add(1, "hour").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(
|
||||||
|
moment().add(1, "hour").toDate()
|
||||||
|
);
|
||||||
|
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment().hours(23).minutes(59).add(8, "day").toDate());
|
const [endDate, setEndDate] = useState<Date | null>(
|
||||||
|
moment().hours(23).minutes(59).add(8, "day").toDate()
|
||||||
|
);
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
const [instructorGender, setInstructorGender] = useState<InstructorGender>("varied");
|
const [instructorGender, setInstructorGender] =
|
||||||
|
useState<InstructorGender>("varied");
|
||||||
|
|
||||||
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
|
||||||
const [released, setReleased] = useState<boolean>(false);
|
const [released, setReleased] = useState<boolean>(false);
|
||||||
@@ -94,20 +146,38 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
const { exams } = useExams();
|
const { exams } = useExams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const classrooms = useMemo(() => groups.filter((e) => e.entity?.id === entity), [entity, groups]);
|
const classrooms = useMemo(
|
||||||
const allowedUsers = useMemo(() => users.filter((u) => mapBy(u.entities, 'id').includes(entity || "")), [users, entity])
|
() => groups.filter((e) => e.entity?.id === entity),
|
||||||
|
[entity, groups]
|
||||||
|
);
|
||||||
|
const allowedUsers = useMemo(
|
||||||
|
() => users.filter((u) => mapBy(u.entities, "id").includes(entity || "")),
|
||||||
|
[users, entity]
|
||||||
|
);
|
||||||
|
|
||||||
const userStudents = useMemo(() => allowedUsers.filter((x) => x.type === "student"), [allowedUsers]);
|
const userStudents = useMemo(
|
||||||
const userTeachers = useMemo(() => allowedUsers.filter((x) => x.type === "teacher"), [allowedUsers]);
|
() => allowedUsers.filter((x) => x.type === "student"),
|
||||||
|
[allowedUsers]
|
||||||
|
);
|
||||||
|
const userTeachers = useMemo(
|
||||||
|
() => allowedUsers.filter((x) => x.type === "teacher"),
|
||||||
|
[allowedUsers]
|
||||||
|
);
|
||||||
|
|
||||||
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents);
|
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } =
|
||||||
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers);
|
useListSearch([["name"], ["email"]], userStudents);
|
||||||
|
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } =
|
||||||
|
useListSearch([["name"], ["email"]], userTeachers);
|
||||||
|
|
||||||
const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE);
|
const { items: studentRows, renderMinimal: renderStudentPagination } =
|
||||||
const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE);
|
usePagination(filteredStudentsRows, SIZE);
|
||||||
|
const { items: teacherRows, renderMinimal: renderTeacherPagination } =
|
||||||
|
usePagination(filteredTeachersRows, SIZE);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
|
setExamIDs((prev) =>
|
||||||
|
prev.filter((x) => selectedModules.includes(x.module))
|
||||||
|
);
|
||||||
}, [selectedModules]);
|
}, [selectedModules]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -117,15 +187,25 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
|
|
||||||
const toggleModule = (module: Module) => {
|
const toggleModule = (module: Module) => {
|
||||||
const modules = selectedModules.filter((x) => x !== module);
|
const modules = selectedModules.filter((x) => x !== module);
|
||||||
setSelectedModules((prev) => (prev.includes(module) ? modules : [...modules, module]));
|
setSelectedModules((prev) =>
|
||||||
|
prev.includes(module) ? modules : [...modules, module]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAssignee = (user: User) => {
|
const toggleAssignee = (user: User) => {
|
||||||
setAssignees((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
|
setAssignees((prev) =>
|
||||||
|
prev.includes(user.id)
|
||||||
|
? prev.filter((a) => a !== user.id)
|
||||||
|
: [...prev, user.id]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTeacher = (user: User) => {
|
const toggleTeacher = (user: User) => {
|
||||||
setTeachers((prev) => (prev.includes(user.id) ? prev.filter((a) => a !== user.id) : [...prev, user.id]));
|
setTeachers((prev) =>
|
||||||
|
prev.includes(user.id)
|
||||||
|
? prev.filter((a) => a !== user.id)
|
||||||
|
: [...prev, user.id]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAssignment = () => {
|
const createAssignment = () => {
|
||||||
@@ -148,7 +228,9 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
autoStart,
|
autoStart,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
toast.success(`The assignment "${name}" has been created successfully!`);
|
toast.success(
|
||||||
|
`The assignment "${name}" has been created successfully!`
|
||||||
|
);
|
||||||
router.push(`/assignments/${result.data.id}`);
|
router.push(`/assignments/${result.data.id}`);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -172,7 +254,10 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
<>
|
<>
|
||||||
<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="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link
|
||||||
|
href="/assignments"
|
||||||
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
|
>
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">Create Assignment</h2>
|
<h2 className="font-bold text-2xl">Create Assignment</h2>
|
||||||
@@ -182,109 +267,180 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
<section className="w-full grid -md:grid-cols-1 md:grid-cols-3 place-items-center -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
<section className="w-full grid -md:grid-cols-1 md:grid-cols-3 place-items-center -md:flex-col -md:items-center -md:gap-12 justify-between gap-8 mt-8 px-8">
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("reading") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("reading")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("reading") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("reading")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-reading top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsBook className="text-white w-7 h-7" />
|
<BsBook className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Reading</span>
|
<span className="ml-8 font-semibold">Reading</span>
|
||||||
{!selectedModules.includes("reading") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("reading") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("reading") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("reading") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("listening") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("listening")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("listening") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("listening")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-listening top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsHeadphones className="text-white w-7 h-7" />
|
<BsHeadphones className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Listening</span>
|
<span className="ml-8 font-semibold">Listening</span>
|
||||||
{!selectedModules.includes("listening") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("listening") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("listening") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("listening") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={
|
||||||
(!selectedModules.includes("level") && selectedModules.length === 0) || selectedModules.includes("level")
|
(!selectedModules.includes("level") &&
|
||||||
|
selectedModules.length === 0) ||
|
||||||
|
selectedModules.includes("level")
|
||||||
? () => toggleModule("level")
|
? () => toggleModule("level")
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("level") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("level")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-level top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsClipboard className="text-white w-7 h-7" />
|
<BsClipboard className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Level</span>
|
<span className="ml-8 font-semibold">Level</span>
|
||||||
{!selectedModules.includes("level") && selectedModules.length === 0 && (
|
{!selectedModules.includes("level") &&
|
||||||
|
selectedModules.length === 0 && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{!selectedModules.includes("level") && selectedModules.length > 0 && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{!selectedModules.includes("level") &&
|
||||||
{selectedModules.includes("level") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
selectedModules.length > 0 && (
|
||||||
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("level") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("writing") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("writing")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("writing") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("writing")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-writing top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsPen className="text-white w-7 h-7" />
|
<BsPen className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Writing</span>
|
<span className="ml-8 font-semibold">Writing</span>
|
||||||
{!selectedModules.includes("writing") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("writing") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("writing") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("writing") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={!selectedModules.includes("level") ? () => toggleModule("speaking") : undefined}
|
onClick={
|
||||||
|
!selectedModules.includes("level")
|
||||||
|
? () => toggleModule("speaking")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
"w-52 relative max-w-xs flex items-center bg-mti-white-alt transition duration-300 ease-in-out border p-5 rounded-xl gap-8 cursor-pointer",
|
||||||
selectedModules.includes("speaking") ? "border-mti-purple-light" : "border-mti-gray-platinum",
|
selectedModules.includes("speaking")
|
||||||
)}>
|
? "border-mti-purple-light"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
<div className="absolute w-16 h-16 flex items-center justify-center rounded-full bg-ielts-speaking top-1/2 -translate-y-1/2 left-0 -translate-x-1/2">
|
||||||
<BsMegaphone className="text-white w-7 h-7" />
|
<BsMegaphone className="text-white w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-8 font-semibold">Speaking</span>
|
<span className="ml-8 font-semibold">Speaking</span>
|
||||||
{!selectedModules.includes("speaking") && !selectedModules.includes("level") && (
|
{!selectedModules.includes("speaking") &&
|
||||||
|
!selectedModules.includes("level") && (
|
||||||
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
<div className="border border-mti-gray-platinum w-8 h-8 rounded-full" />
|
||||||
)}
|
)}
|
||||||
{selectedModules.includes("level") && <BsXCircle className="text-mti-red-light w-8 h-8" />}
|
{selectedModules.includes("level") && (
|
||||||
{selectedModules.includes("speaking") && <BsCheckCircle className="text-mti-purple-light w-8 h-8" />}
|
<BsXCircle className="text-mti-red-light w-8 h-8" />
|
||||||
|
)}
|
||||||
|
{selectedModules.includes("speaking") && (
|
||||||
|
<BsCheckCircle className="text-mti-purple-light w-8 h-8" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
onChange={(e) => setName(e)}
|
||||||
|
defaultValue={name}
|
||||||
|
label="Assignment Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Entity"
|
label="Entity"
|
||||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||||
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
||||||
defaultValue={{ value: entities[0]?.id, label: entities[0]?.label }}
|
defaultValue={{
|
||||||
|
value: entities[0]?.id,
|
||||||
|
label: entities[0]?.label,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="w-full grid -md:grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Limit Start Date *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Limit Start Date *
|
||||||
|
</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip z-10",
|
"hover:border-mti-purple tooltip z-10",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
popperClassName="!z-20"
|
popperClassName="!z-20"
|
||||||
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
|
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
|
||||||
@@ -295,12 +451,14 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">End Date *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
End Date *
|
||||||
|
</label>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"hover:border-mti-purple tooltip z-10",
|
"hover:border-mti-purple tooltip z-10",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
popperClassName="!z-20"
|
popperClassName="!z-20"
|
||||||
filterTime={(date) => moment(date).isAfter(startDate)}
|
filterTime={(date) => moment(date).isAfter(startDate)}
|
||||||
@@ -314,13 +472,19 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
|
|
||||||
{selectedModules.includes("speaking") && (
|
{selectedModules.includes("speaking") && (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Speaking Instructor's Gender
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: instructorGender,
|
value: instructorGender,
|
||||||
label: capitalize(instructorGender),
|
label: capitalize(instructorGender),
|
||||||
}}
|
}}
|
||||||
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
|
onChange={(value) =>
|
||||||
|
value
|
||||||
|
? setInstructorGender(value.value as InstructorGender)
|
||||||
|
: null
|
||||||
|
}
|
||||||
disabled={!selectedModules.includes("speaking")}
|
disabled={!selectedModules.includes("speaking")}
|
||||||
options={[
|
options={[
|
||||||
{ value: "male", label: "Male" },
|
{ value: "male", label: "Male" },
|
||||||
@@ -340,11 +504,16 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
<div className="grid md:grid-cols-2 w-full gap-4">
|
<div className="grid md:grid-cols-2 w-full gap-4">
|
||||||
{selectedModules.map((module) => (
|
{selectedModules.map((module) => (
|
||||||
<div key={module} className="flex flex-col gap-3 w-full">
|
<div key={module} className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">{capitalize(module)} Exam</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
{capitalize(module)} Exam
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: examIDs.find((e) => e.module === module)?.id || null,
|
value:
|
||||||
label: examIDs.find((e) => e.module === module)?.id || "",
|
examIDs.find((e) => e.module === module)?.id ||
|
||||||
|
null,
|
||||||
|
label:
|
||||||
|
examIDs.find((e) => e.module === module)?.id || "",
|
||||||
}}
|
}}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
value
|
value
|
||||||
@@ -352,7 +521,9 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
...prev.filter((x) => x.module !== module),
|
...prev.filter((x) => x.module !== module),
|
||||||
{ id: value.value!, module },
|
{ id: value.value!, module },
|
||||||
])
|
])
|
||||||
: setExamIDs((prev) => prev.filter((x) => x.module !== module))
|
: setExamIDs((prev) =>
|
||||||
|
prev.filter((x) => x.module !== module)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
options={exams
|
options={exams
|
||||||
.filter((x) => !x.isDiagnostic && x.module === module)
|
.filter((x) => !x.isDiagnostic && x.module === module)
|
||||||
@@ -366,25 +537,40 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="w-full flex flex-col gap-4">
|
<section className="w-full flex flex-col gap-4">
|
||||||
<span className="font-semibold">Assignees ({assignees.length} selected)</span>
|
<span className="font-semibold">
|
||||||
|
Assignees ({assignees.length} selected)
|
||||||
|
</span>
|
||||||
<div className="grid grid-cols-5 gap-4">
|
<div className="grid grid-cols-5 gap-4">
|
||||||
{classrooms.map((g) => (
|
{classrooms.map((g) => (
|
||||||
<button
|
<button
|
||||||
key={g.id}
|
key={g.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
|
const groupStudentIds = users.reduce<string[]>((acc, u) => {
|
||||||
|
if (g.participants.includes(u.id)) {
|
||||||
|
acc.push(u.id);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
if (groupStudentIds.every((u) => assignees.includes(u))) {
|
if (groupStudentIds.every((u) => assignees.includes(u))) {
|
||||||
setAssignees((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
|
setAssignees((prev) =>
|
||||||
|
prev.filter((a) => !groupStudentIds.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setAssignees((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
|
setAssignees((prev) => [
|
||||||
|
...prev.filter((a) => !groupStudentIds.includes(a)),
|
||||||
|
...groupStudentIds,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
|
users
|
||||||
"!bg-mti-purple-light !text-white",
|
.filter((u) => g.participants.includes(u.id))
|
||||||
)}>
|
.every((u) => assignees.includes(u.id)) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{g.name}
|
{g.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -402,9 +588,12 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
assignees.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
|
assignees.includes(user.id)
|
||||||
|
? "border-mti-purple"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
)}
|
)}
|
||||||
key={user.id}>
|
key={user.id}
|
||||||
|
>
|
||||||
<span className="flex flex-col gap-0 justify-center">
|
<span className="flex flex-col gap-0 justify-center">
|
||||||
<span className="font-semibold">{user.name}</span>
|
<span className="font-semibold">{user.name}</span>
|
||||||
<span className="text-sm opacity-80">{user.email}</span>
|
<span className="text-sm opacity-80">{user.email}</span>
|
||||||
@@ -430,25 +619,43 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
|
|
||||||
{user.type !== "teacher" && (
|
{user.type !== "teacher" && (
|
||||||
<section className="w-full flex flex-col gap-3">
|
<section className="w-full flex flex-col gap-3">
|
||||||
<span className="font-semibold">Teachers ({teachers.length} selected)</span>
|
<span className="font-semibold">
|
||||||
|
Teachers ({teachers.length} selected)
|
||||||
|
</span>
|
||||||
<div className="grid grid-cols-5 gap-4">
|
<div className="grid grid-cols-5 gap-4">
|
||||||
{classrooms.map((g) => (
|
{classrooms.map((g) => (
|
||||||
<button
|
<button
|
||||||
key={g.id}
|
key={g.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const groupStudentIds = users.filter((u) => g.participants.includes(u.id)).map((u) => u.id);
|
const groupStudentIds = users.reduce<string[]>(
|
||||||
|
(acc, u) => {
|
||||||
|
if (g.participants.includes(u.id)) {
|
||||||
|
acc.push(u.id);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
if (groupStudentIds.every((u) => teachers.includes(u))) {
|
if (groupStudentIds.every((u) => teachers.includes(u))) {
|
||||||
setTeachers((prev) => prev.filter((a) => !groupStudentIds.includes(a)));
|
setTeachers((prev) =>
|
||||||
|
prev.filter((a) => !groupStudentIds.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setTeachers((prev) => [...prev.filter((a) => !groupStudentIds.includes(a)), ...groupStudentIds]);
|
setTeachers((prev) => [
|
||||||
|
...prev.filter((a) => !groupStudentIds.includes(a)),
|
||||||
|
...groupStudentIds,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
|
users
|
||||||
"!bg-mti-purple-light !text-white",
|
.filter((u) => g.participants.includes(u.id))
|
||||||
)}>
|
.every((u) => teachers.includes(u.id)) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{g.name}
|
{g.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -466,9 +673,12 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
"p-4 flex flex-col gap-2 rounded-xl border cursor-pointer w-72",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
teachers.includes(user.id) ? "border-mti-purple" : "border-mti-gray-platinum",
|
teachers.includes(user.id)
|
||||||
|
? "border-mti-purple"
|
||||||
|
: "border-mti-gray-platinum"
|
||||||
)}
|
)}
|
||||||
key={user.id}>
|
key={user.id}
|
||||||
|
>
|
||||||
<span className="flex flex-col gap-0 justify-center">
|
<span className="flex flex-col gap-0 justify-center">
|
||||||
<span className="font-semibold">{user.name}</span>
|
<span className="font-semibold">{user.name}</span>
|
||||||
<span className="text-sm opacity-80">{user.email}</span>
|
<span className="text-sm opacity-80">{user.email}</span>
|
||||||
@@ -487,16 +697,30 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-4 w-full items-end">
|
<div className="flex gap-4 w-full items-end">
|
||||||
<Checkbox isChecked={variant === "full"} onChange={() => setVariant((prev) => (prev === "full" ? "partial" : "full"))}>
|
<Checkbox
|
||||||
|
isChecked={variant === "full"}
|
||||||
|
onChange={() =>
|
||||||
|
setVariant((prev) => (prev === "full" ? "partial" : "full"))
|
||||||
|
}
|
||||||
|
>
|
||||||
Full length exams
|
Full length exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={generateMultiple}
|
||||||
|
onChange={() => setGenerateMultiple((d) => !d)}
|
||||||
|
>
|
||||||
Generate different exams
|
Generate different exams
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={released} onChange={() => setReleased((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={released}
|
||||||
|
onChange={() => setReleased((d) => !d)}
|
||||||
|
>
|
||||||
Auto release results
|
Auto release results
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox isChecked={autoStart} onChange={() => setAutostart((d) => !d)}>
|
<Checkbox
|
||||||
|
isChecked={autoStart}
|
||||||
|
onChange={() => setAutostart((d) => !d)}
|
||||||
|
>
|
||||||
Auto start exam
|
Auto start exam
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -506,7 +730,8 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => router.push("/assignments")}
|
onClick={() => router.push("/assignments")}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -522,7 +747,8 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
}
|
}
|
||||||
className="w-full max-w-[200px]"
|
className="w-full max-w-[200px]"
|
||||||
onClick={createAssignment}
|
onClick={createAssignment}
|
||||||
isLoading={isLoading}>
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,59 +28,137 @@ 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"]))
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
!checkAccess(user, [
|
||||||
|
"admin",
|
||||||
|
"developer",
|
||||||
|
"corporate",
|
||||||
|
"teacher",
|
||||||
|
"mastercorporate",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return redirect("/");
|
||||||
|
const isAdmin = checkAccess(user, ["developer", "admin"]);
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await (checkAccess(user, ["developer", "admin"]) ? getEntitiesWithRoles() : getEntitiesWithRoles(entityIDS));
|
const entities = await (isAdmin
|
||||||
|
? getEntitiesWithRoles()
|
||||||
|
: getEntitiesWithRoles(entityIDS));
|
||||||
|
|
||||||
const allowedEntities = findAllowedEntities(user, entities, "view_assignments")
|
const allowedEntities = findAllowedEntities(
|
||||||
|
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 users =
|
return {
|
||||||
await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
|
props: serialize({ user, users, entities: allowedEntities, assignments }),
|
||||||
|
};
|
||||||
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 (
|
||||||
<>
|
<>
|
||||||
@@ -96,7 +174,10 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
<>
|
<>
|
||||||
<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
|
||||||
|
href="/dashboard"
|
||||||
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
|
>
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">Assignments</h2>
|
<h2 className="font-bold text-2xl">Assignments</h2>
|
||||||
@@ -107,35 +188,56 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
<span className="text-lg font-bold">Active Assignments Status</span>
|
<span className="text-lg font-bold">Active Assignments Status</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span>
|
<span>
|
||||||
<b>Total:</b> {activeAssignments.reduce((acc, curr) => acc + curr.results.length, 0)}/
|
<b>Total:</b>{" "}
|
||||||
{activeAssignments.reduce((acc, curr) => curr.exams.length + acc, 0)}
|
{activeAssignments.reduce(
|
||||||
|
(acc, curr) => acc + curr.results.length,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
/
|
||||||
|
{activeAssignments.reduce(
|
||||||
|
(acc, curr) => curr.exams.length + acc,
|
||||||
|
0
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Active Assignments ({activeAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Active Assignments ({activeAssignments.length})
|
||||||
|
</h2>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderActive()}
|
{renderActive()}
|
||||||
{paginationActive()}
|
{paginationActive()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{activeItems.map((a) => (
|
{activeItems.map((a) => (
|
||||||
<AssignmentCard {...a} entityObj={findBy(entities, 'id', a.entity)} users={users} onClick={() => router.push(`/assignments/${a.id}`)} key={a.id} />
|
<AssignmentCard
|
||||||
|
{...a}
|
||||||
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
|
users={users}
|
||||||
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
|
key={a.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Planned Assignments ({plannedAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Planned Assignments ({plannedAssignments.length})
|
||||||
|
</h2>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderPlanned()}
|
{renderPlanned()}
|
||||||
{paginationPlanned()}
|
{paginationPlanned()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Link
|
<Link
|
||||||
href={entitiesAllowCreate.length > 0 ? "/assignments/creator" : ""}
|
href={
|
||||||
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">
|
entitiesAllowCreate.length > 0 ? "/assignments/creator" : ""
|
||||||
|
}
|
||||||
|
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"
|
||||||
|
>
|
||||||
<BsPlus className="text-6xl" />
|
<BsPlus className="text-6xl" />
|
||||||
<span className="text-lg">New Assignment</span>
|
<span className="text-lg">New Assignment</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -143,9 +245,9 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
onClick={
|
onClick={
|
||||||
mapBy(entitiesAllowEdit, 'id').includes(a.entity || "")
|
mapBy(entitiesAllowEdit, "id").includes(a.entity || "")
|
||||||
? () => router.push(`/assignments/creator/${a.id}`)
|
? () => router.push(`/assignments/creator/${a.id}`)
|
||||||
: () => router.push(`/assignments/${a.id}`)
|
: () => router.push(`/assignments/${a.id}`)
|
||||||
}
|
}
|
||||||
@@ -156,7 +258,9 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
</section>
|
</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">
|
||||||
|
Past Assignments ({pastAssignments.length})
|
||||||
|
</h2>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderPast()}
|
{renderPast()}
|
||||||
{paginationPast()}
|
{paginationPast()}
|
||||||
@@ -166,18 +270,22 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
onClick={() => router.push(`/assignments/${a.id}`)}
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
allowDownload
|
allowDownload
|
||||||
allowArchive={mapBy(entitiesAllowArchive, 'id').includes(a.entity || "")}
|
allowArchive={mapBy(entitiesAllowArchive, "id").includes(
|
||||||
|
a.entity || ""
|
||||||
|
)}
|
||||||
allowExcelDownload
|
allowExcelDownload
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Assignments start expired ({startExpiredAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Assignments start expired ({startExpiredAssignments.length})
|
||||||
|
</h2>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderExpired()}
|
{renderExpired()}
|
||||||
{paginationExpired()}
|
{paginationExpired()}
|
||||||
@@ -187,18 +295,22 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
{...a}
|
{...a}
|
||||||
users={users}
|
users={users}
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
onClick={() => router.push(`/assignments/${a.id}`)}
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
allowDownload
|
allowDownload
|
||||||
allowArchive={mapBy(entitiesAllowArchive, 'id').includes(a.entity || "")}
|
allowArchive={mapBy(entitiesAllowArchive, "id").includes(
|
||||||
|
a.entity || ""
|
||||||
|
)}
|
||||||
allowExcelDownload
|
allowExcelDownload
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col gap-4">
|
<section className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-semibold">Archived Assignments ({archivedAssignments.length})</h2>
|
<h2 className="text-2xl font-semibold">
|
||||||
|
Archived Assignments ({archivedAssignments.length})
|
||||||
|
</h2>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderArchived()}
|
{renderArchived()}
|
||||||
{paginationArchived()}
|
{paginationArchived()}
|
||||||
@@ -210,7 +322,7 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
users={users}
|
users={users}
|
||||||
onClick={() => router.push(`/assignments/${a.id}`)}
|
onClick={() => router.push(`/assignments/${a.id}`)}
|
||||||
key={a.id}
|
key={a.id}
|
||||||
entityObj={findBy(entities, 'id', a.entity)}
|
entityObj={findBy(entities, "id", a.entity)}
|
||||||
allowDownload
|
allowDownload
|
||||||
allowUnarchive
|
allowUnarchive
|
||||||
allowExcelDownload
|
allowExcelDownload
|
||||||
|
|||||||
@@ -18,47 +18,93 @@ 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 users = await getSpecificUsers([...group.participants, group.admin]);
|
|
||||||
const groupWithUser = convertToUsers(group, users);
|
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,
|
||||||
|
group: groupWithUser,
|
||||||
|
users: linkedUsers.filter((x) => (isAdmin(user) ? true : !isAdmin(x))),
|
||||||
|
entity,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
},
|
||||||
|
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) {
|
||||||
@@ -66,36 +112,73 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
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 (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to remove ${selectedUsers.length} participant${
|
||||||
|
selectedUsers.length === 1 ? "" : "s"
|
||||||
|
} from this group?`
|
||||||
|
)
|
||||||
|
)
|
||||||
return;
|
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}`, {
|
||||||
|
participants: group.participants
|
||||||
|
.map((x) => x.id)
|
||||||
|
.filter((x) => !selectedUsers.includes(x)),
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The group has been updated successfully!");
|
toast.success("The group has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -110,13 +193,24 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
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 (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to add ${selectedUsers.length} participant${
|
||||||
|
selectedUsers.length === 1 ? "" : "s"
|
||||||
|
} to this group?`
|
||||||
|
)
|
||||||
|
)
|
||||||
return;
|
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}`, {
|
||||||
|
participants: [
|
||||||
|
...group.participants.map((x) => x.id),
|
||||||
|
...selectedUsers,
|
||||||
|
],
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The group has been updated successfully!");
|
toast.success("The group has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -189,7 +283,8 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
<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 />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">{group.name}</h2>
|
<h2 className="font-bold text-2xl">{group.name}</h2>
|
||||||
@@ -200,14 +295,16 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
<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 />
|
<BsTag />
|
||||||
<span className="text-xs">Rename Classroom</span>
|
<span className="text-xs">Rename Classroom</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={deleteGroup}
|
onClick={deleteGroup}
|
||||||
disabled={isLoading || !canDeleteClassroom}
|
disabled={isLoading || !canDeleteClassroom}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<BsTrash />
|
<BsTrash />
|
||||||
<span className="text-xs">Delete Classroom</span>
|
<span className="text-xs">Delete Classroom</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -219,7 +316,8 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
<BsBuilding className="text-xl" /> {entity.label}
|
<BsBuilding className="text-xl" /> {entity.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<BsFillPersonVcardFill className="text-xl" /> {getUserName(group.admin)}
|
<BsFillPersonVcardFill className="text-xl" />{" "}
|
||||||
|
{getUserName(group.admin)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,14 +329,20 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsAdding(true)}
|
onClick={() => setIsAdding(true)}
|
||||||
disabled={isLoading || !canAddParticipants}
|
disabled={isLoading || !canAddParticipants}
|
||||||
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"
|
||||||
|
>
|
||||||
<BsPlus />
|
<BsPlus />
|
||||||
<span className="text-xs">Add Participants</span>
|
<span className="text-xs">Add Participants</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={removeParticipants}
|
onClick={removeParticipants}
|
||||||
disabled={selectedUsers.length === 0 || isLoading || !canRemoveParticipants}
|
disabled={
|
||||||
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">
|
selectedUsers.length === 0 ||
|
||||||
|
isLoading ||
|
||||||
|
!canRemoveParticipants
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
<BsTrash />
|
<BsTrash />
|
||||||
<span className="text-xs">Remove Participants</span>
|
<span className="text-xs">Remove Participants</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -249,14 +353,20 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsAdding(false)}
|
onClick={() => setIsAdding(false)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<BsX />
|
<BsX />
|
||||||
<span className="text-xs">Discard Selection</span>
|
<span className="text-xs">Discard Selection</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={addParticipants}
|
onClick={addParticipants}
|
||||||
disabled={selectedUsers.length === 0 || isLoading || !canAddParticipants}
|
disabled={
|
||||||
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">
|
selectedUsers.length === 0 ||
|
||||||
|
isLoading ||
|
||||||
|
!canAddParticipants
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
<BsPlus />
|
<BsPlus />
|
||||||
<span className="text-xs">Add Participants</span>
|
<span className="text-xs">Add Participants</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -268,26 +378,53 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
{renderMinimal()}
|
{renderMinimal()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-4">
|
<div className="flex items-center gap-2 mt-4">
|
||||||
{['student', 'teacher', 'corporate'].map((type) => (
|
{["student", "teacher", "corporate"].map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const typeUsers = mapBy(filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type), 'id')
|
const typeUsers = mapBy(
|
||||||
|
filterBy(
|
||||||
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
|
"type",
|
||||||
|
type
|
||||||
|
),
|
||||||
|
"id"
|
||||||
|
);
|
||||||
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
||||||
setSelectedUsers((prev) => prev.filter((a) => !typeUsers.includes(a)));
|
setSelectedUsers((prev) =>
|
||||||
|
prev.filter((a) => !typeUsers.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedUsers((prev) => [...prev.filter((a) => !typeUsers.includes(a)), ...typeUsers]);
|
setSelectedUsers((prev) => [
|
||||||
|
...prev.filter((a) => !typeUsers.includes(a)),
|
||||||
|
...typeUsers,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type).length === 0}
|
disabled={
|
||||||
|
filterBy(
|
||||||
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
|
"type",
|
||||||
|
type
|
||||||
|
).length === 0
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
"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(
|
||||||
filterBy(isAdding ? nonParticipantUsers : group.participants, 'type', type).every((u) => selectedUsers.includes(u.id)) &&
|
isAdding ? nonParticipantUsers : group.participants,
|
||||||
"!bg-mti-purple-light !text-white",
|
"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)}
|
{capitalize(type)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -298,20 +435,25 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
{items.map((u) => (
|
{items.map((u) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleUser(u)}
|
onClick={() => toggleUser(u)}
|
||||||
disabled={isAdding ? !canAddParticipants : !canRemoveParticipants}
|
disabled={
|
||||||
|
isAdding ? !canAddParticipants : !canRemoveParticipants
|
||||||
|
}
|
||||||
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="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="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} />
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-semibold">{getUserName(u)}</span>
|
<span className="font-semibold">{getUserName(u)}</span>
|
||||||
<span className="opacity-80 text-sm">{USER_TYPE_LABELS[u.type]}</span>
|
<span className="opacity-80 text-sm">
|
||||||
|
{USER_TYPE_LABELS[u.type]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -326,13 +468,19 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
|
? moment(u.subscriptionExpirationDate).format(
|
||||||
|
"DD/MM/YYYY"
|
||||||
|
)
|
||||||
|
: "Unlimited"}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Last Login">
|
<Tooltip tooltip="Last Login">
|
||||||
<BsClockFill />
|
<BsClockFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
{u.lastLogin
|
||||||
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
|
: "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,24 +22,57 @@ 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 {
|
||||||
|
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";
|
||||||
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);
|
||||||
|
|
||||||
@@ -55,27 +88,48 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
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`, {
|
||||||
|
name,
|
||||||
|
participants: selectedUsers,
|
||||||
|
admin: user.id,
|
||||||
|
entity,
|
||||||
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
toast.success("Your group has been created successfully!");
|
toast.success("Your group has been created successfully!");
|
||||||
router.replace(`/classrooms/${result.data.id}`);
|
router.replace(`/classrooms/${result.data.id}`);
|
||||||
@@ -87,7 +141,10 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
.finally(() => setIsLoading(false));
|
.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 (
|
||||||
<>
|
<>
|
||||||
@@ -107,7 +164,8 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
<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 />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">Create Classroom</h2>
|
<h2 className="font-bold text-2xl">Create Classroom</h2>
|
||||||
@@ -116,7 +174,8 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={createGroup}
|
onClick={createGroup}
|
||||||
disabled={!name.trim() || !entity || isLoading}
|
disabled={!name.trim() || !entity || isLoading}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<BsCheck />
|
<BsCheck />
|
||||||
<span className="text-xs">Create Classroom</span>
|
<span className="text-xs">Create Classroom</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -126,46 +185,67 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
<div className="grid grid-cols-2 gap-4 place-items-end">
|
<div className="grid grid-cols-2 gap-4 place-items-end">
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<span className="font-semibold text-xl">Classroom Name:</span>
|
<span className="font-semibold text-xl">Classroom Name:</span>
|
||||||
<Input name="name" onChange={setName} type="text" placeholder="Classroom A" />
|
<Input
|
||||||
|
name="name"
|
||||||
|
onChange={setName}
|
||||||
|
type="text"
|
||||||
|
placeholder="Classroom A"
|
||||||
|
/>
|
||||||
</div>
|
</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">Entity:</span>
|
<span className="font-semibold text-xl">Entity:</span>
|
||||||
<Select
|
<Select
|
||||||
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
options={entities.map((e) => ({ value: e.id, label: e.label }))}
|
||||||
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
onChange={(v) => setEntity(v ? v.value! : undefined)}
|
||||||
defaultValue={{value: entities[0]?.id, label: entities[0]?.label}}
|
defaultValue={{
|
||||||
|
value: entities[0]?.id,
|
||||||
|
label: entities[0]?.label,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<span className="font-semibold text-xl">Participants ({selectedUsers.length} selected):</span>
|
<span className="font-semibold text-xl">
|
||||||
|
Participants ({selectedUsers.length} selected):
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
{renderMinimal()}
|
{renderMinimal()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-4">
|
<div className="flex items-center gap-2 mt-4">
|
||||||
{['student', 'teacher', 'corporate'].map((type) => (
|
{["student", "teacher", "corporate"].map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const typeUsers = mapBy(filterBy(entityUsers, 'type', type), 'id')
|
const typeUsers = mapBy(
|
||||||
|
filterBy(entityUsers, "type", type),
|
||||||
|
"id"
|
||||||
|
);
|
||||||
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
if (typeUsers.every((u) => selectedUsers.includes(u))) {
|
||||||
setSelectedUsers((prev) => prev.filter((a) => !typeUsers.includes(a)));
|
setSelectedUsers((prev) =>
|
||||||
|
prev.filter((a) => !typeUsers.includes(a))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedUsers((prev) => [...prev.filter((a) => !typeUsers.includes(a)), ...typeUsers]);
|
setSelectedUsers((prev) => [
|
||||||
|
...prev.filter((a) => !typeUsers.includes(a)),
|
||||||
|
...typeUsers,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={filterBy(entityUsers, 'type', type).length === 0}
|
disabled={filterBy(entityUsers, "type", type).length === 0}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
"disabled:grayscale disabled:hover:bg-mti-purple-ultralight disabled:hover:text-mti-purple disabled:cursor-not-allowed",
|
"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).length > 0 &&
|
||||||
filterBy(entityUsers, 'type', type).every((u) => selectedUsers.includes(u.id)) &&
|
filterBy(entityUsers, "type", type).every((u) =>
|
||||||
"!bg-mti-purple-light !text-white",
|
selectedUsers.includes(u.id)
|
||||||
)}>
|
) &&
|
||||||
|
"!bg-mti-purple-light !text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{capitalize(type)}
|
{capitalize(type)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -181,15 +261,18 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
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="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="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} />
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-semibold">{getUserName(u)}</span>
|
<span className="font-semibold">{getUserName(u)}</span>
|
||||||
<span className="opacity-80 text-sm">{USER_TYPE_LABELS[u.type]}</span>
|
<span className="opacity-80 text-sm">
|
||||||
|
{USER_TYPE_LABELS[u.type]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -204,13 +287,17 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
|
? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY")
|
||||||
|
: "Unlimited"}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Last Login">
|
<Tooltip tooltip="Last Login">
|
||||||
<BsClockFill />
|
<BsClockFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
{u.lastLogin
|
||||||
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
|
: "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -27,22 +27,41 @@ 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))));
|
|
||||||
|
const users = await getSpecificUsers(
|
||||||
|
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 {
|
return {
|
||||||
props: serialize({ user, groups: groupsWithUsers, entities: allowedEntities }),
|
props: serialize({
|
||||||
|
user,
|
||||||
|
groups: groupsWithUsers,
|
||||||
|
entities: allowedEntities,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -59,39 +78,60 @@ const SEARCH_FIELDS = [
|
|||||||
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(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"create_classroom"
|
||||||
|
);
|
||||||
const [showImport, setShowImport] = useState(false);
|
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">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<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">Classroom</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
|
Classroom
|
||||||
|
</span>
|
||||||
{group.name}
|
{group.name}
|
||||||
</span>
|
</span>
|
||||||
<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">Admin</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
|
Admin
|
||||||
|
</span>
|
||||||
{getUserName(group.admin)}
|
{getUserName(group.admin)}
|
||||||
</span>
|
</span>
|
||||||
{!!group.entity && (
|
{!!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">Entity</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
{findBy(entities, 'id', group.entity)?.label}
|
Entity
|
||||||
|
</span>
|
||||||
|
{findBy(entities, "id", group.entity)?.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<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>
|
Participants
|
||||||
|
</span>
|
||||||
|
<span className="bg-mti-purple-light/50 px-2">
|
||||||
|
{group.participants.length}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{group.participants.slice(0, 3).map(getUserName).join(", ")}{' '}
|
{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> : ""}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
@@ -103,7 +143,8 @@ export default function Home({ user, groups, entities }: Props) {
|
|||||||
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} />
|
<BsPlus size={40} />
|
||||||
<span className="font-semibold">Create Classroom</span>
|
<span className="font-semibold">Create Classroom</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -123,24 +164,33 @@ export default function Home({ user, groups, entities }: Props) {
|
|||||||
<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}
|
||||||
|
onClose={() => setShowImport(false)}
|
||||||
|
maxWidth="max-w-[85%]"
|
||||||
|
>
|
||||||
|
<StudentClassroomTransfer
|
||||||
|
user={user}
|
||||||
|
entities={entities}
|
||||||
|
onFinish={() => setShowImport(false)}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<h2 className="font-bold text-2xl">Classrooms</h2>
|
<h2 className="font-bold text-2xl">Classrooms</h2>
|
||||||
{entitiesAllowCreate.length !== 0 && <button
|
{entitiesAllowCreate.length !== 0 && (
|
||||||
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-row gap-3 items-center py-1.5 px-4 text-lg",
|
"flex flex-row gap-3 items-center py-1.5 px-4 text-lg",
|
||||||
"bg-mti-purple-light border border-mti-purple-light rounded-xl text-white",
|
"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",
|
"hover:bg-white hover:text-mti-purple-light transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
onClick={() => setShowImport(true)}
|
onClick={() => setShowImport(true)}
|
||||||
>
|
>
|
||||||
<FaFileUpload className="w-5 h-5" />
|
<FaFileUpload className="w-5 h-5" />
|
||||||
Transfer Students
|
Transfer Students
|
||||||
</button>
|
</button>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,18 +6,13 @@ 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,
|
||||||
|
usersCount,
|
||||||
|
groupsCount,
|
||||||
|
students,
|
||||||
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
] = await Promise.all([
|
||||||
|
getEntities(undefined, { _id: 0, id: 1, label: 1 }),
|
||||||
|
countUsersByTypes(["student", "teacher", "corporate", "mastercorporate"]),
|
||||||
|
countGroups(),
|
||||||
|
getUsers(
|
||||||
{ type: "student" },
|
{ type: "student" },
|
||||||
10,
|
10,
|
||||||
{
|
{
|
||||||
averageLevel: -1,
|
averageLevel: -1,
|
||||||
},
|
},
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
|
getUsers(
|
||||||
const usersCount = await countUsersByTypes([
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"corporate",
|
|
||||||
"mastercorporate",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const latestStudents = await getUsers(
|
|
||||||
{ type: "student" },
|
{ type: "student" },
|
||||||
10,
|
10,
|
||||||
{
|
{
|
||||||
registrationDate: -1,
|
registrationDate: -1,
|
||||||
},
|
},
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
const latestTeachers = await getUsers(
|
getUsers(
|
||||||
{ type: "teacher" },
|
{ type: "teacher" },
|
||||||
10,
|
10,
|
||||||
{
|
{
|
||||||
registrationDate: -1,
|
registrationDate: -1,
|
||||||
},
|
},
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, 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;
|
||||||
@@ -71,37 +69,41 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
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,
|
||||||
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
userCounts,
|
||||||
|
assignmentsCount,
|
||||||
|
groupsCount,
|
||||||
|
] = await Promise.all([
|
||||||
|
getUsers(
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
10,
|
10,
|
||||||
{ averageLevel: -1 },
|
{ averageLevel: -1 },
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
const latestStudents = await getUsers(
|
getUsers(
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
10,
|
10,
|
||||||
{ registrationDate: -1 },
|
{ registrationDate: -1 },
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
const latestTeachers = await getUsers(
|
getUsers(
|
||||||
{
|
{
|
||||||
type: "teacher",
|
type: "teacher",
|
||||||
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
||||||
},
|
},
|
||||||
10,
|
10,
|
||||||
{ registrationDate: -1 },
|
{ registrationDate: -1 },
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
|
countAllowedUsers(user, entities),
|
||||||
const userCounts = await countAllowedUsers(user, entities);
|
countEntitiesAssignments(entitiesIDS, {
|
||||||
|
archived: { $ne: true },
|
||||||
const assignmentsCount = await countEntitiesAssignments(
|
}),
|
||||||
entitiesIDS,
|
countGroupsByEntities(entitiesIDS),
|
||||||
{ archived: { $ne: true } }
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
const groupsCount = await countGroupsByEntities(entitiesIDS);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({
|
props: serialize({
|
||||||
|
|||||||
@@ -6,17 +6,12 @@ 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 [
|
||||||
|
students,
|
||||||
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
usersCount,
|
||||||
|
entities,
|
||||||
|
groupsCount,
|
||||||
|
] = await Promise.all([
|
||||||
|
getUsers(
|
||||||
{ type: "student" },
|
{ type: "student" },
|
||||||
10,
|
10,
|
||||||
{
|
{ averageLevel: -1 },
|
||||||
averageLevel: -1,
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
},
|
),
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
getUsers(
|
||||||
);
|
|
||||||
|
|
||||||
const usersCount = await countUsersByTypes([
|
|
||||||
"student",
|
|
||||||
"teacher",
|
|
||||||
"corporate",
|
|
||||||
"mastercorporate",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const latestStudents = await getUsers(
|
|
||||||
{ type: "student" },
|
{ type: "student" },
|
||||||
10,
|
10,
|
||||||
{
|
{ registrationDate: -1 },
|
||||||
registrationDate: -1,
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
},
|
),
|
||||||
{id:1, name: 1, email: 1, profilePicture: 1 }
|
getUsers(
|
||||||
);
|
|
||||||
const latestTeachers = await getUsers(
|
|
||||||
{ type: "teacher" },
|
{ type: "teacher" },
|
||||||
10,
|
10,
|
||||||
{
|
{ registrationDate: -1 },
|
||||||
registrationDate: -1,
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
},
|
),
|
||||||
{ id:1,name: 1, email: 1, profilePicture: 1 }
|
countUsersByTypes(["student", "teacher", "corporate", "mastercorporate"]),
|
||||||
);
|
getEntities(undefined, { _id: 0, id: 1, label: 1 }),
|
||||||
|
countGroups(),
|
||||||
|
]);
|
||||||
|
|
||||||
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 [
|
||||||
|
students,
|
||||||
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
userCounts,
|
||||||
|
assignmentsCount,
|
||||||
|
groupsCount,
|
||||||
|
] = await Promise.all([
|
||||||
|
getUsers(
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
10,
|
10,
|
||||||
{ averageLevel: -1 },
|
{ averageLevel: -1 },
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
|
getUsers(
|
||||||
const latestStudents = await getUsers(
|
|
||||||
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
10,
|
10,
|
||||||
{ registrationDate: -1 },
|
{ registrationDate: -1 },
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
|
getUsers(
|
||||||
const latestTeachers = await getUsers(
|
|
||||||
{
|
{
|
||||||
type: "teacher",
|
type: "teacher",
|
||||||
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
"entities.id": { $in: mapBy(allowedTeacherEntities, "id") },
|
||||||
},
|
},
|
||||||
10,
|
10,
|
||||||
{ registrationDate: -1 },
|
{ registrationDate: -1 },
|
||||||
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
{ _id: 0, id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
);
|
),
|
||||||
|
countAllowedUsers(user, entities),
|
||||||
const userCounts = await countAllowedUsers(user, entities);
|
countEntitiesAssignments(entitiesIDS, { archived: { $ne: true } }),
|
||||||
|
countGroupsByEntities(entitiesIDS),
|
||||||
const assignmentsCount = await countEntitiesAssignments(entitiesIDS, {
|
]);
|
||||||
archived: { $ne: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const groupsCount = await 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) => {
|
||||||
|
a.exams.forEach((e: { module: Module; id: string }) => {
|
||||||
|
acc.push({
|
||||||
module: e.module,
|
module: e.module,
|
||||||
id: e.id,
|
id: e.id,
|
||||||
key: `${e.module}_${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,7 +53,8 @@ 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([
|
||||||
|
getEntitiesUsers(
|
||||||
mapBy(filteredEntities, "id"),
|
mapBy(filteredEntities, "id"),
|
||||||
{
|
{
|
||||||
type: "student",
|
type: "student",
|
||||||
@@ -67,14 +69,13 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
levels: 1,
|
levels: 1,
|
||||||
registrationDate: 1,
|
registrationDate: 1,
|
||||||
}
|
}
|
||||||
);
|
),
|
||||||
|
getEntitiesAssignments(entityIDS),
|
||||||
const assignments = await 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">
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import { getEntityWithRoles } from "@/utils/entities.be";
|
|||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { doesEntityAllow } from "@/utils/permissions";
|
import { doesEntityAllow } from "@/utils/permissions";
|
||||||
import { getUserName, isAdmin } from "@/utils/users";
|
import { getUserName, isAdmin } from "@/utils/users";
|
||||||
import { filterAllowedUsers, getEntitiesUsers, getEntityUsers, getUsers } from "@/utils/users.be";
|
import {
|
||||||
|
filterAllowedUsers,
|
||||||
|
getEntitiesUsers,
|
||||||
|
getEntityUsers,
|
||||||
|
getUsers,
|
||||||
|
} from "@/utils/users.be";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -42,14 +47,18 @@ import {
|
|||||||
BsX,
|
BsX,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import entities from "../../api/entities";
|
||||||
|
|
||||||
const expirationDateColor = (date: Date) => {
|
const expirationDateColor = (date: Date) => {
|
||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new Date());
|
const today = moment(new Date());
|
||||||
|
|
||||||
if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light";
|
if (today.add(1, "days").isAfter(momentDate))
|
||||||
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
|
return "!bg-mti-red-ultralight border-mti-red-light";
|
||||||
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light";
|
if (today.add(3, "days").isAfter(momentDate))
|
||||||
|
return "!bg-mti-rose-ultralight border-mti-rose-light";
|
||||||
|
if (today.add(7, "days").isAfter(momentDate))
|
||||||
|
return "!bg-mti-orange-ultralight border-mti-orange-light";
|
||||||
};
|
};
|
||||||
|
|
||||||
const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({
|
const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({
|
||||||
@@ -57,26 +66,62 @@ const CURRENCIES_OPTIONS = CURRENCIES.map(({ label, currency }) => ({
|
|||||||
label,
|
label,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, params }) => {
|
const USER_DATA_SCHEMA = {
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
type: 1,
|
||||||
|
profilePicture: 1,
|
||||||
|
email: 1,
|
||||||
|
lastLogin: 1,
|
||||||
|
subscriptionExpirationDate: 1,
|
||||||
|
entities: 1,
|
||||||
|
corporateInformation: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(
|
||||||
|
async ({ req, params }) => {
|
||||||
const user = req.session.user as User;
|
const user = req.session.user as User;
|
||||||
|
|
||||||
if (!user) return redirect("/login")
|
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 entity = await getEntityWithRoles(id);
|
const entity = await getEntityWithRoles(id);
|
||||||
if (!entity) return redirect("/entities")
|
if (!entity) return redirect("/entities");
|
||||||
|
|
||||||
if (!doesEntityAllow(user, entity, "view_entities")) return redirect(`/entities`)
|
if (!doesEntityAllow(user, entity, "view_entities"))
|
||||||
|
return redirect(`/entities`);
|
||||||
const linkedUsers = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(user.entities, 'id'),
|
const [linkedUsers, entityUsers] = await Promise.all([
|
||||||
{ $and: [{ type: { $ne: "developer" } }, { type: { $ne: "admin" } }] }))
|
isAdmin(user)
|
||||||
const entityUsers = await (isAdmin(user) ? getEntityUsers(id) : filterAllowedUsers(user, [entity]));
|
? getUsers({}, 0, {}, USER_DATA_SCHEMA)
|
||||||
|
: getEntitiesUsers(
|
||||||
|
mapBy(user.entities, "id"),
|
||||||
|
{
|
||||||
|
$and: [
|
||||||
|
{ type: { $ne: "developer" } },
|
||||||
|
{ type: { $ne: "admin" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
USER_DATA_SCHEMA
|
||||||
|
),
|
||||||
|
isAdmin(user)
|
||||||
|
? getEntityUsers(
|
||||||
|
id,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
id: { $ne: user.id },
|
||||||
|
},
|
||||||
|
USER_DATA_SCHEMA
|
||||||
|
)
|
||||||
|
: filterAllowedUsers(user, [entity], USER_DATA_SCHEMA),
|
||||||
|
]);
|
||||||
|
|
||||||
const usersWithRole = entityUsers.map((u) => {
|
const usersWithRole = entityUsers.map((u) => {
|
||||||
const e = u.entities.find((e) => e.id === id);
|
const e = u?.entities?.find((e) => e.id === id);
|
||||||
return { ...u, role: findBy(entity.roles, 'id', e?.role) };
|
return { ...u, role: findBy(entity.roles, "id", e?.role) };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -84,10 +129,14 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, params }) =>
|
|||||||
user,
|
user,
|
||||||
entity,
|
entity,
|
||||||
users: usersWithRole,
|
users: usersWithRole,
|
||||||
linkedUsers: linkedUsers.filter(x => x.id !== user.id && !mapBy(entityUsers, 'id').includes(x.id)),
|
linkedUsers: linkedUsers.filter(
|
||||||
|
(x) => !mapBy(entityUsers, "id").includes(x.id)
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
},
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
|
||||||
type UserWithRole = User & { role?: Role };
|
type UserWithRole = User & { role?: Role };
|
||||||
|
|
||||||
@@ -102,34 +151,52 @@ export default function Home({ user, entity, users, linkedUsers }: 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 [expiryDate, setExpiryDate] = useState(entity?.expiryDate)
|
const [expiryDate, setExpiryDate] = useState(entity?.expiryDate);
|
||||||
const [paymentPrice, setPaymentPrice] = useState(entity?.payment?.price)
|
const [paymentPrice, setPaymentPrice] = useState(entity?.payment?.price);
|
||||||
const [paymentCurrency, setPaymentCurrency] = useState(entity?.payment?.currency)
|
const [paymentCurrency, setPaymentCurrency] = useState(
|
||||||
|
entity?.payment?.currency
|
||||||
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const canRenameEntity = useEntityPermission(user, entity, "rename_entity")
|
const canRenameEntity = useEntityPermission(user, entity, "rename_entity");
|
||||||
const canViewRoles = useEntityPermission(user, entity, "view_entity_roles")
|
const canViewRoles = useEntityPermission(user, entity, "view_entity_roles");
|
||||||
const canDeleteEntity = useEntityPermission(user, entity, "delete_entity")
|
const canDeleteEntity = useEntityPermission(user, entity, "delete_entity");
|
||||||
|
|
||||||
const canAddMembers = useEntityPermission(user, entity, "add_to_entity")
|
const canAddMembers = useEntityPermission(user, entity, "add_to_entity");
|
||||||
const canRemoveMembers = useEntityPermission(user, entity, "remove_from_entity")
|
const canRemoveMembers = useEntityPermission(
|
||||||
|
user,
|
||||||
|
entity,
|
||||||
|
"remove_from_entity"
|
||||||
|
);
|
||||||
|
|
||||||
const canAssignRole = useEntityPermission(user, entity, "assign_to_role")
|
const canAssignRole = useEntityPermission(user, entity, "assign_to_role");
|
||||||
const canPay = useEntityPermission(user, entity, 'pay_entity')
|
const canPay = useEntityPermission(user, entity, "pay_entity");
|
||||||
|
|
||||||
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 (!canRemoveMembers) return;
|
if (!canRemoveMembers) return;
|
||||||
if (!confirm(`Are you sure you want to remove ${selectedUsers.length} member${selectedUsers.length === 1 ? "" : "s"} from this entity?`))
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to remove ${selectedUsers.length} member${
|
||||||
|
selectedUsers.length === 1 ? "" : "s"
|
||||||
|
} from this entity?`
|
||||||
|
)
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/entities/${entity.id}/users`, { add: false, members: selectedUsers })
|
.patch(`/api/entities/${entity.id}/users`, {
|
||||||
|
add: false,
|
||||||
|
members: selectedUsers,
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The entity has been updated successfully!");
|
toast.success("The entity has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -145,13 +212,24 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
const addParticipants = () => {
|
const addParticipants = () => {
|
||||||
if (selectedUsers.length === 0) return;
|
if (selectedUsers.length === 0) return;
|
||||||
if (!canAddMembers || !isAdding) return;
|
if (!canAddMembers || !isAdding) return;
|
||||||
if (!confirm(`Are you sure you want to add ${selectedUsers.length} member${selectedUsers.length === 1 ? "" : "s"} to this entity?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to add ${selectedUsers.length} member${
|
||||||
|
selectedUsers.length === 1 ? "" : "s"
|
||||||
|
} to this entity?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const defaultRole = findBy(entity.roles, 'isDefault', true)!
|
const defaultRole = findBy(entity.roles, "isDefault", true)!;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/api/entities/${entity.id}/users`, { add: true, members: selectedUsers, role: defaultRole.id })
|
.patch(`/api/entities/${entity.id}/users`, {
|
||||||
|
add: true,
|
||||||
|
members: selectedUsers,
|
||||||
|
role: defaultRole.id,
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The entity has been updated successfully!");
|
toast.success("The entity has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -206,7 +284,9 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.patch(`/api/entities/${entity.id}`, { payment: { price: paymentPrice, currency: paymentCurrency } })
|
.patch(`/api/entities/${entity.id}`, {
|
||||||
|
payment: { price: paymentPrice, currency: paymentCurrency },
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("The entity has been updated successfully!");
|
toast.success("The entity has been updated successfully!");
|
||||||
router.replace(router.asPath);
|
router.replace(router.asPath);
|
||||||
@@ -221,9 +301,13 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
const editLicenses = () => {
|
const editLicenses = () => {
|
||||||
if (!isAdmin(user)) return;
|
if (!isAdmin(user)) return;
|
||||||
|
|
||||||
const licenses = prompt("Update the number of licenses:", (entity.licenses || 0).toString());
|
const licenses = prompt(
|
||||||
|
"Update the number of licenses:",
|
||||||
|
(entity.licenses || 0).toString()
|
||||||
|
);
|
||||||
if (!licenses) return;
|
if (!licenses) return;
|
||||||
if (!parseInt(licenses) || parseInt(licenses) <= 0) return toast.error("Write a valid number of licenses!")
|
if (!parseInt(licenses) || parseInt(licenses) <= 0)
|
||||||
|
return toast.error("Write a valid number of licenses!");
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
@@ -259,8 +343,8 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const assignUsersToRole = (role: string) => {
|
const assignUsersToRole = (role: string) => {
|
||||||
if (!canAssignRole) return
|
if (!canAssignRole) return;
|
||||||
if (selectedUsers.length === 0) return
|
if (selectedUsers.length === 0) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
@@ -274,7 +358,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
toast.error("Something went wrong!");
|
toast.error("Something went wrong!");
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderCard = (u: UserWithRole) => {
|
const renderCard = (u: UserWithRole) => {
|
||||||
return (
|
return (
|
||||||
@@ -285,8 +369,9 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
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="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="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} />
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
@@ -311,13 +396,17 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
|
? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY")
|
||||||
|
: "Unlimited"}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Last Login">
|
<Tooltip tooltip="Last Login">
|
||||||
<BsClockFill />
|
<BsClockFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
{u.lastLogin
|
||||||
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
|
: "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -345,10 +434,14 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
href="/entities"
|
href="/entities"
|
||||||
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 />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">{entity.label} {isAdmin(user) && `- ${entity.licenses || 0} licenses`}</h2>
|
<h2 className="font-bold text-2xl">
|
||||||
|
{entity.label}{" "}
|
||||||
|
{isAdmin(user) && `- ${entity.licenses || 0} licenses`}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isAdmin(user) && canPay && (
|
{!isAdmin(user) && canPay && (
|
||||||
@@ -357,11 +450,15 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!entity.expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(entity.expiryDate),
|
!entity.expiryDate
|
||||||
"bg-white border-mti-gray-platinum",
|
? "!bg-mti-green-ultralight !border-mti-green-light"
|
||||||
)}>
|
: expirationDateColor(entity.expiryDate),
|
||||||
|
"bg-white border-mti-gray-platinum"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!entity.expiryDate && "Unlimited"}
|
{!entity.expiryDate && "Unlimited"}
|
||||||
{entity.expiryDate && moment(entity.expiryDate).format("DD/MM/YYYY")}
|
{entity.expiryDate &&
|
||||||
|
moment(entity.expiryDate).format("DD/MM/YYYY")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -369,7 +466,8 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={renameGroup}
|
onClick={renameGroup}
|
||||||
disabled={isLoading || !canRenameEntity}
|
disabled={isLoading || !canRenameEntity}
|
||||||
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 />
|
<BsTag />
|
||||||
<span className="text-xs">Rename Entity</span>
|
<span className="text-xs">Rename Entity</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -377,7 +475,8 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={editLicenses}
|
onClick={editLicenses}
|
||||||
disabled={isLoading || !isAdmin(user)}
|
disabled={isLoading || !isAdmin(user)}
|
||||||
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"
|
||||||
|
>
|
||||||
<BsHash />
|
<BsHash />
|
||||||
<span className="text-xs">Edit Licenses</span>
|
<span className="text-xs">Edit Licenses</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -385,14 +484,16 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => router.push(`/entities/${entity.id}/roles`)}
|
onClick={() => router.push(`/entities/${entity.id}/roles`)}
|
||||||
disabled={isLoading || !canViewRoles}
|
disabled={isLoading || !canViewRoles}
|
||||||
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"
|
||||||
|
>
|
||||||
<BsPerson />
|
<BsPerson />
|
||||||
<span className="text-xs">Edit Roles</span>
|
<span className="text-xs">Edit Roles</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={deleteGroup}
|
onClick={deleteGroup}
|
||||||
disabled={isLoading || !canDeleteEntity}
|
disabled={isLoading || !canDeleteEntity}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<BsTrash />
|
<BsTrash />
|
||||||
<span className="text-xs">Delete Entity</span>
|
<span className="text-xs">Delete Entity</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -410,8 +511,10 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-2 w-full max-w-[200px] 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",
|
||||||
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
|
!expiryDate
|
||||||
"transition duration-300 ease-in-out",
|
? "!bg-mti-green-ultralight !border-mti-green-light"
|
||||||
|
: expirationDateColor(expiryDate),
|
||||||
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
filterDate={(date) => moment(date).isAfter(new Date())}
|
filterDate={(date) => moment(date).isAfter(new Date())}
|
||||||
dateFormat="dd/MM/yyyy"
|
dateFormat="dd/MM/yyyy"
|
||||||
@@ -425,8 +528,10 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
"p-2 w-full max-w-[200px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
!expiryDate ? "!bg-mti-green-ultralight !border-mti-green-light" : expirationDateColor(expiryDate),
|
!expiryDate
|
||||||
"bg-white border-mti-gray-platinum",
|
? "!bg-mti-green-ultralight !border-mti-green-light"
|
||||||
|
: expirationDateColor(expiryDate),
|
||||||
|
"bg-white border-mti-gray-platinum"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Unlimited
|
Unlimited
|
||||||
@@ -435,17 +540,21 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isChecked={!!expiryDate}
|
isChecked={!!expiryDate}
|
||||||
onChange={(checked: boolean) => setExpiryDate(checked ? entity.expiryDate || new Date() : null)}
|
onChange={(checked: boolean) =>
|
||||||
|
setExpiryDate(
|
||||||
|
checked ? entity.expiryDate || new Date() : null
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Enable expiry date
|
Enable expiry date
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={updateExpiryDate}
|
onClick={updateExpiryDate}
|
||||||
disabled={expiryDate === entity.expiryDate}
|
disabled={expiryDate === entity.expiryDate}
|
||||||
className="flex w-fit text-nowrap 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">
|
className="flex w-fit text-nowrap 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"
|
||||||
|
>
|
||||||
<BsCheck />
|
<BsCheck />
|
||||||
<span className="text-xs">Apply Change</span>
|
<span className="text-xs">Apply Change</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -457,25 +566,34 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<div className="w-full max-w-xl flex items-center gap-4">
|
<div className="w-full max-w-xl flex items-center gap-4">
|
||||||
<Input
|
<Input
|
||||||
name="paymentValue"
|
name="paymentValue"
|
||||||
onChange={(e) => setPaymentPrice(e ? parseInt(e) : undefined)}
|
onChange={(e) =>
|
||||||
|
setPaymentPrice(e ? parseInt(e) : undefined)
|
||||||
|
}
|
||||||
type="number"
|
type="number"
|
||||||
defaultValue={entity.payment?.price || 0}
|
defaultValue={entity.payment?.price || 0}
|
||||||
thin
|
thin
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-4 !py-2 !w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none",
|
"px-4 !py-2 !w-full text-sm font-normal placeholder:text-mti-gray-cool bg-white rounded-full border border-mti-gray-platinum focus:outline-none"
|
||||||
)}
|
)}
|
||||||
options={CURRENCIES_OPTIONS}
|
options={CURRENCIES_OPTIONS}
|
||||||
value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)}
|
value={CURRENCIES_OPTIONS.find(
|
||||||
onChange={(value) => setPaymentCurrency(value?.value ?? undefined)}
|
(c) => c.value === paymentCurrency
|
||||||
|
)}
|
||||||
|
onChange={(value) =>
|
||||||
|
setPaymentCurrency(value?.value ?? undefined)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={updatePayment}
|
onClick={updatePayment}
|
||||||
disabled={!paymentPrice || paymentPrice <= 0 || !paymentCurrency}
|
disabled={
|
||||||
className="flex w-fit text-nowrap 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">
|
!paymentPrice || paymentPrice <= 0 || !paymentCurrency
|
||||||
|
}
|
||||||
|
className="flex w-fit text-nowrap 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"
|
||||||
|
>
|
||||||
<BsCheck />
|
<BsCheck />
|
||||||
<span className="text-xs">Apply Change</span>
|
<span className="text-xs">Apply Change</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -485,28 +603,40 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<span className="font-semibold text-xl">Members ({users.length})</span>
|
<span className="font-semibold text-xl">
|
||||||
|
Members ({users.length})
|
||||||
|
</span>
|
||||||
{!isAdding && (
|
{!isAdding && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAdding(true)}
|
onClick={() => setIsAdding(true)}
|
||||||
disabled={isLoading || !canAddMembers}
|
disabled={isLoading || !canAddMembers}
|
||||||
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"
|
||||||
|
>
|
||||||
<BsPlus />
|
<BsPlus />
|
||||||
<span className="text-xs">Add Members</span>
|
<span className="text-xs">Add Members</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
disabled={isLoading || !canAssignRole || selectedUsers.length === 0}
|
disabled={
|
||||||
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">
|
isLoading || !canAssignRole || selectedUsers.length === 0
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
<BsPerson />
|
<BsPerson />
|
||||||
<span className="text-xs">Assign Role</span>
|
<span className="text-xs">Assign Role</span>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuItems anchor="bottom" className="bg-white rounded-xl shadow drop-shadow border mt-1 flex flex-col">
|
<MenuItems
|
||||||
|
anchor="bottom"
|
||||||
|
className="bg-white rounded-xl shadow drop-shadow border mt-1 flex flex-col"
|
||||||
|
>
|
||||||
{entity.roles.map((role) => (
|
{entity.roles.map((role) => (
|
||||||
<MenuItem key={role.id}>
|
<MenuItem key={role.id}>
|
||||||
<button onClick={() => assignUsersToRole(role.id)} className="p-4 hover:bg-neutral-100 w-32">
|
<button
|
||||||
|
onClick={() => assignUsersToRole(role.id)}
|
||||||
|
className="p-4 hover:bg-neutral-100 w-32"
|
||||||
|
>
|
||||||
{role.label}
|
{role.label}
|
||||||
</button>
|
</button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -516,8 +646,11 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={removeParticipants}
|
onClick={removeParticipants}
|
||||||
disabled={selectedUsers.length === 0 || isLoading || !canRemoveMembers}
|
disabled={
|
||||||
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">
|
selectedUsers.length === 0 || isLoading || !canRemoveMembers
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
<BsTrash />
|
<BsTrash />
|
||||||
<span className="text-xs">Remove Members</span>
|
<span className="text-xs">Remove Members</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -528,16 +661,22 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setIsAdding(false)}
|
onClick={() => setIsAdding(false)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<BsX />
|
<BsX />
|
||||||
<span className="text-xs">Discard Selection</span>
|
<span className="text-xs">Discard Selection</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={addParticipants}
|
onClick={addParticipants}
|
||||||
disabled={selectedUsers.length === 0 || isLoading || !canAddMembers}
|
disabled={
|
||||||
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">
|
selectedUsers.length === 0 || isLoading || !canAddMembers
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
<BsPlus />
|
<BsPlus />
|
||||||
<span className="text-xs">Add Members ({selectedUsers.length})</span>
|
<span className="text-xs">
|
||||||
|
Add Members ({selectedUsers.length})
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -546,7 +685,13 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<CardList<User | UserWithRole>
|
<CardList<User | UserWithRole>
|
||||||
list={isAdding ? linkedUsers : users}
|
list={isAdding ? linkedUsers : users}
|
||||||
renderCard={renderCard}
|
renderCard={renderCard}
|
||||||
searchFields={[["name"], ["email"], ["corporateInformation", "companyInformation", "name"], ["role", "label"], ["type"]]}
|
searchFields={[
|
||||||
|
["name"],
|
||||||
|
["email"],
|
||||||
|
["corporateInformation", "companyInformation", "name"],
|
||||||
|
["role", "label"],
|
||||||
|
["type"],
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -20,21 +20,41 @@ 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);
|
||||||
|
|
||||||
@@ -49,19 +69,31 @@ export default function Home({ user, users }: Props) {
|
|||||||
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>(
|
||||||
|
[["name"], ["corporateInformation", "companyInformation", "name"]],
|
||||||
|
users
|
||||||
|
);
|
||||||
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
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`, {
|
||||||
|
label,
|
||||||
|
licenses,
|
||||||
|
members: selectedUsers,
|
||||||
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
toast.success("Your entity has been created successfully!");
|
toast.success("Your entity has been created successfully!");
|
||||||
router.replace(`/entities/${result.data.id}`);
|
router.replace(`/entities/${result.data.id}`);
|
||||||
@@ -73,7 +105,10 @@ export default function Home({ user, users }: Props) {
|
|||||||
.finally(() => setIsLoading(false));
|
.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 (
|
||||||
<>
|
<>
|
||||||
@@ -93,7 +128,8 @@ export default function Home({ user, users }: Props) {
|
|||||||
<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 />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">Create Entity</h2>
|
<h2 className="font-bold text-2xl">Create Entity</h2>
|
||||||
@@ -102,7 +138,8 @@ export default function Home({ user, users }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={createGroup}
|
onClick={createGroup}
|
||||||
disabled={!label.trim() || licenses <= 0 || isLoading}
|
disabled={!label.trim() || licenses <= 0 || isLoading}
|
||||||
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">
|
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"
|
||||||
|
>
|
||||||
<BsCheck />
|
<BsCheck />
|
||||||
<span className="text-xs">Create Entity</span>
|
<span className="text-xs">Create Entity</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -112,17 +149,30 @@ export default function Home({ user, users }: Props) {
|
|||||||
<div className="w-full grid grid-cols-2 gap-4">
|
<div className="w-full grid grid-cols-2 gap-4">
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<span className="font-semibold text-xl">Entity Label:</span>
|
<span className="font-semibold text-xl">Entity Label:</span>
|
||||||
<Input name="name" onChange={setLabel} type="text" placeholder="Entity A" />
|
<Input
|
||||||
|
name="name"
|
||||||
|
onChange={setLabel}
|
||||||
|
type="text"
|
||||||
|
placeholder="Entity A"
|
||||||
|
/>
|
||||||
</div>
|
</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
|
||||||
|
name="licenses"
|
||||||
|
min={0}
|
||||||
|
onChange={(v) => setLicenses(parseInt(v))}
|
||||||
|
type="number"
|
||||||
|
placeholder="12"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<span className="font-semibold text-xl">Members ({selectedUsers.length} selected):</span>
|
<span className="font-semibold text-xl">
|
||||||
|
Members ({selectedUsers.length} selected):
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
{renderSearch()}
|
{renderSearch()}
|
||||||
@@ -139,15 +189,18 @@ export default function Home({ user, users }: Props) {
|
|||||||
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="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="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} />
|
<img src={u.profilePicture} alt={u.name} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-semibold">{getUserName(u)}</span>
|
<span className="font-semibold">{getUserName(u)}</span>
|
||||||
<span className="opacity-80 text-sm">{USER_TYPE_LABELS[u.type]}</span>
|
<span className="opacity-80 text-sm">
|
||||||
|
{USER_TYPE_LABELS[u.type]}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,13 +215,17 @@ export default function Home({ user, users }: Props) {
|
|||||||
<Tooltip tooltip="Expiration Date">
|
<Tooltip tooltip="Expiration Date">
|
||||||
<BsStopwatchFill />
|
<BsStopwatchFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.subscriptionExpirationDate ? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited"}
|
{u.subscriptionExpirationDate
|
||||||
|
? moment(u.subscriptionExpirationDate).format("DD/MM/YYYY")
|
||||||
|
: "Unlimited"}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Tooltip tooltip="Last Login">
|
<Tooltip tooltip="Last Login">
|
||||||
<BsClockFill />
|
<BsClockFill />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{u.lastLogin ? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm") : "N/A"}
|
{u.lastLogin
|
||||||
|
? moment(u.lastLogin).format("DD/MM/YYYY - HH:mm")
|
||||||
|
: "N/A"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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,77 +21,103 @@ 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,
|
||||||
|
assignment,
|
||||||
|
exams = [],
|
||||||
|
destinationURL = "/exam",
|
||||||
|
session,
|
||||||
|
}) => {
|
||||||
|
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) {
|
||||||
if (!activeAssignmentFilter(assignment)) return
|
if (!activeAssignmentFilter(assignment)) return;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "INIT_EXAM", payload: {
|
type: "INIT_EXAM",
|
||||||
|
payload: {
|
||||||
exams: exams.sort(sortByModule),
|
exams: exams.sort(sortByModule),
|
||||||
modules: exams
|
modules: exams
|
||||||
.map((x) => x!)
|
.map((x) => x!)
|
||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map((x) => x!.module),
|
.map((x) => x!.module),
|
||||||
assignment
|
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 (
|
||||||
<>
|
<>
|
||||||
@@ -104,10 +130,15 @@ const Page: React.FC<Props> = ({ user, assignment, exams = [], destinationURL =
|
|||||||
<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,79 +21,87 @@ 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 (!["admin", "developer"].includes(user.type) && !assignment.assignees.includes(user.id)) return redirect("/exercises")
|
|
||||||
|
|
||||||
const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
|
|
||||||
const session = await getSessionByAssignment(assignmentID)
|
|
||||||
|
|
||||||
|
if (!assignment) return redirect("/exam");
|
||||||
if (
|
if (
|
||||||
filterBy(assignment.results, 'user', user.id) ||
|
!["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.startDate).isBefore(moment()) ||
|
||||||
moment(assignment.endDate).isAfter(moment())
|
moment(assignment.endDate).isAfter(moment())
|
||||||
)
|
)
|
||||||
return redirect("/exam")
|
return redirect("/exam");
|
||||||
|
const [exams, session] = await Promise.all([
|
||||||
|
getExamsByIds(uniqBy(assignment.exams, "id")),
|
||||||
|
getSessionByAssignment(assignmentID),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, assignment, exams, session })
|
props: serialize({ user, assignment, exams, session }),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user }),
|
props: serialize({ user }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
},
|
||||||
|
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",
|
||||||
|
payload: {
|
||||||
exams: exams.sort(sortByModule),
|
exams: exams.sort(sortByModule),
|
||||||
modules: exams
|
modules: exams
|
||||||
.map((x) => x!)
|
.map((x) => x!)
|
||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map((x) => x!.module),
|
.map((x) => x!.module),
|
||||||
assignment
|
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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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(
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
user.id,
|
||||||
|
{
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
|
||||||
const assignments = await getAssignmentsByAssignee(user.id, {
|
|
||||||
archived: { $ne: true },
|
archived: { $ne: true },
|
||||||
});
|
},
|
||||||
const sessions = await getSessionsByUser(user.id, 0, {
|
{
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
startDate: 1,
|
||||||
|
endDate: 1,
|
||||||
|
exams: 1,
|
||||||
|
results: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: { startDate: 1 },
|
||||||
|
}
|
||||||
|
)) as Assignment[];
|
||||||
|
|
||||||
|
const sessions = await getSessionsByUser(
|
||||||
|
user.id,
|
||||||
|
0,
|
||||||
|
{
|
||||||
"assignment.id": { $in: mapBy(assignments, "id") },
|
"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) => {
|
||||||
|
if (e.assignee === user.id)
|
||||||
|
acc.push({
|
||||||
module: e.module,
|
module: e.module,
|
||||||
id: e.id,
|
id: e.id,
|
||||||
key: `${e.module}_${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,9 +222,7 @@ 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))
|
|
||||||
.map((a) => (
|
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
key={a.id}
|
key={a.id}
|
||||||
assignment={a}
|
assignment={a}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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,6 +204,7 @@ export default function Page(props: Props) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
return null;
|
||||||
})}
|
})}
|
||||||
</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,23 +35,34 @@ 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);
|
||||||
|
|
||||||
@@ -59,46 +72,67 @@ 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({
|
||||||
|
user,
|
||||||
|
users,
|
||||||
|
assignments,
|
||||||
|
entities,
|
||||||
|
gradingSystems,
|
||||||
|
isAdmin,
|
||||||
|
pendingSessionIds,
|
||||||
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore(
|
||||||
|
(state) => [
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
state.setSelectedUser,
|
state.setSelectedUser,
|
||||||
state.training,
|
state.training,
|
||||||
state.setTraining,
|
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(
|
||||||
|
() =>
|
||||||
|
groupByDate(
|
||||||
stats.filter((x) => {
|
stats.filter((x) => {
|
||||||
if (
|
if (
|
||||||
(
|
(x.module === "writing" || x.module === "speaking") &&
|
||||||
x.module === "writing" || x.module === "speaking") &&
|
!x.isDisabled &&
|
||||||
!x.isDisabled && Array.isArray(x.solutions) &&
|
Array.isArray(x.solutions) &&
|
||||||
!x.solutions.every((y) => Object.keys(y).includes("evaluation")
|
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}),
|
})
|
||||||
), [stats])
|
),
|
||||||
|
[stats]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
||||||
|
|
||||||
@@ -120,7 +154,8 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -129,8 +164,14 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
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;
|
||||||
@@ -143,9 +184,14 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
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<
|
||||||
|
Record<string, Stat[]>
|
||||||
|
>((accumulator, moduleAndTimestamp) => {
|
||||||
const timestamp = moduleAndTimestamp.split("-")[1];
|
const timestamp = moduleAndTimestamp.split("-")[1];
|
||||||
if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) {
|
if (
|
||||||
|
allStats.includes(timestamp) &&
|
||||||
|
!accumulator.hasOwnProperty(timestamp)
|
||||||
|
) {
|
||||||
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
accumulator[timestamp] = groupedStatsByDate[timestamp];
|
||||||
}
|
}
|
||||||
return accumulator;
|
return accumulator;
|
||||||
@@ -155,17 +201,22 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredStats = useMemo(() =>
|
const filteredStats = useMemo(
|
||||||
Object.keys(filterStatsByDate(groupedStats))
|
() =>
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a)),
|
Object.keys(filterStatsByDate(groupedStats)).sort(
|
||||||
|
(a, b) => parseInt(b) - parseInt(a)
|
||||||
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[groupedStats, filter])
|
[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
|
||||||
@@ -185,7 +236,11 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEvaluationPolling(pendingSessionIds ? pendingSessionIds : [], "records", user.id);
|
useEvaluationPolling(
|
||||||
|
pendingSessionIds ? pendingSessionIds : [],
|
||||||
|
"records",
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -201,7 +256,12 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<RecordFilter user={user} isAdmin={isAdmin} entities={entities} filterState={{ filter: filter, setFilter: setFilter }}>
|
<RecordFilter
|
||||||
|
user={user}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
entities={entities}
|
||||||
|
filterState={{ filter: filter, setFilter: setFilter }}
|
||||||
|
>
|
||||||
{training && (
|
{training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">
|
<div className="font-semibold text-2xl mr-4">
|
||||||
@@ -211,19 +271,25 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4 disabled:cursor-not-allowed",
|
"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",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
disabled={selectedTrainingExams.length == 0}
|
disabled={selectedTrainingExams.length == 0}
|
||||||
onClick={handleTrainingContentSubmission}>
|
onClick={handleTrainingContentSubmission}
|
||||||
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</RecordFilter>
|
</RecordFilter>
|
||||||
|
|
||||||
|
|
||||||
{filteredStats.length > 0 && !isStatsLoading && (
|
{filteredStats.length > 0 && !isStatsLoading && (
|
||||||
<CardList list={filteredStats} renderCard={customContent} searchFields={[]} pageSize={30} className="lg:!grid-cols-3" />
|
<CardList
|
||||||
|
list={filteredStats}
|
||||||
|
renderCard={customContent}
|
||||||
|
searchFields={[]}
|
||||||
|
pageSize={30}
|
||||||
|
className="lg:!grid-cols-3"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{filteredStats.length === 0 && !isStatsLoading && (
|
{filteredStats.length === 0 && !isStatsLoading && (
|
||||||
<span className="font-semibold ml-1">No record to display...</span>
|
<span className="font-semibold ml-1">No record to display...</span>
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ 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";
|
||||||
@@ -26,26 +32,55 @@ 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, [
|
||||||
const permissions = await getUserPermissions(user.id);
|
"admin",
|
||||||
const entities = isAdmin(user) ? await getEntitiesWithRoles() : await getEntitiesWithRoles(mapBy(user.entities, 'id'))
|
"developer",
|
||||||
const allUsers = await getUsers()
|
"corporate",
|
||||||
const gradingSystems = await getGradingSystemByEntities(mapBy(entities, 'id'))
|
"teacher",
|
||||||
const entitiesGrading = entities.map(e => gradingSystems.find(g => g.entity === e.id) || { entity: e.id, steps: CEFR_STEPS })
|
"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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, permissions, entities, allUsers, entitiesGrading }),
|
props: serialize({
|
||||||
|
user,
|
||||||
|
permissions,
|
||||||
|
entities,
|
||||||
|
allUsers,
|
||||||
|
entitiesGrading,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -53,19 +88,45 @@ 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({
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
permissions,
|
||||||
|
allUsers,
|
||||||
|
entitiesGrading,
|
||||||
|
}: Props) {
|
||||||
const [modalOpen, setModalOpen] = useState<string>();
|
const [modalOpen, setModalOpen] = useState<string>();
|
||||||
const router = useRouter()
|
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 (
|
||||||
<>
|
<>
|
||||||
@@ -80,7 +141,11 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)} maxWidth="max-w-[85%]">
|
<Modal
|
||||||
|
isOpen={modalOpen === "batchCreateUser"}
|
||||||
|
onClose={() => setModalOpen(undefined)}
|
||||||
|
maxWidth="max-w-[85%]"
|
||||||
|
>
|
||||||
<BatchCreateUser
|
<BatchCreateUser
|
||||||
user={user}
|
user={user}
|
||||||
entities={entitiesAllowCreateUser}
|
entities={entitiesAllowCreateUser}
|
||||||
@@ -88,7 +153,10 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
onFinish={() => setModalOpen(undefined)}
|
onFinish={() => setModalOpen(undefined)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal isOpen={modalOpen === "batchCreateCode"} onClose={() => setModalOpen(undefined)}>
|
<Modal
|
||||||
|
isOpen={modalOpen === "batchCreateCode"}
|
||||||
|
onClose={() => setModalOpen(undefined)}
|
||||||
|
>
|
||||||
<BatchCodeGenerator
|
<BatchCodeGenerator
|
||||||
entities={entitiesAllowCreateCodes}
|
entities={entitiesAllowCreateCodes}
|
||||||
user={user}
|
user={user}
|
||||||
@@ -97,7 +165,10 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
onFinish={() => setModalOpen(undefined)}
|
onFinish={() => setModalOpen(undefined)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal isOpen={modalOpen === "createCode"} onClose={() => setModalOpen(undefined)}>
|
<Modal
|
||||||
|
isOpen={modalOpen === "createCode"}
|
||||||
|
onClose={() => setModalOpen(undefined)}
|
||||||
|
>
|
||||||
<CodeGenerator
|
<CodeGenerator
|
||||||
entities={entitiesAllowCreateCode}
|
entities={entitiesAllowCreateCode}
|
||||||
user={user}
|
user={user}
|
||||||
@@ -105,7 +176,10 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
onFinish={() => setModalOpen(undefined)}
|
onFinish={() => setModalOpen(undefined)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal isOpen={modalOpen === "createUser"} onClose={() => setModalOpen(undefined)}>
|
<Modal
|
||||||
|
isOpen={modalOpen === "createUser"}
|
||||||
|
onClose={() => setModalOpen(undefined)}
|
||||||
|
>
|
||||||
<UserCreator
|
<UserCreator
|
||||||
user={user}
|
user={user}
|
||||||
entities={entitiesAllowCreateUsers}
|
entities={entitiesAllowCreateUsers}
|
||||||
@@ -114,7 +188,10 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
onFinish={() => setModalOpen(undefined)}
|
onFinish={() => setModalOpen(undefined)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal isOpen={modalOpen === "gradingSystem"} onClose={() => setModalOpen(undefined)}>
|
<Modal
|
||||||
|
isOpen={modalOpen === "gradingSystem"}
|
||||||
|
onClose={() => setModalOpen(undefined)}
|
||||||
|
>
|
||||||
<CorporateGradingSystem
|
<CorporateGradingSystem
|
||||||
user={user}
|
user={user}
|
||||||
entitiesGrading={entitiesGrading}
|
entitiesGrading={entitiesGrading}
|
||||||
@@ -125,7 +202,12 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
|
|
||||||
<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(
|
||||||
|
user,
|
||||||
|
getTypesOfUser(["teacher"]),
|
||||||
|
permissions,
|
||||||
|
"viewCodes"
|
||||||
|
) && (
|
||||||
<div className="w-full grid grid-cols-2 gap-4">
|
<div className="w-full grid grid-cols-2 gap-4">
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsCode}
|
Icon={BsCode}
|
||||||
@@ -159,7 +241,12 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
onClick={() => setModalOpen("batchCreateUser")}
|
onClick={() => setModalOpen("batchCreateUser")}
|
||||||
disabled={entitiesAllowCreateUsers.length === 0}
|
disabled={entitiesAllowCreateUsers.length === 0}
|
||||||
/>
|
/>
|
||||||
{checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && (
|
{checkAccess(user, [
|
||||||
|
"admin",
|
||||||
|
"corporate",
|
||||||
|
"developer",
|
||||||
|
"mastercorporate",
|
||||||
|
]) && (
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsGearFill}
|
Icon={BsGearFill}
|
||||||
label="Grading System"
|
label="Grading System"
|
||||||
|
|||||||
@@ -28,67 +28,104 @@ 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({
|
||||||
|
user,
|
||||||
|
students,
|
||||||
|
entities,
|
||||||
|
assignments,
|
||||||
|
sessions,
|
||||||
|
exams,
|
||||||
|
}: Props) {
|
||||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(moment().add(1, 'month').toDate());
|
const [endDate, setEndDate] = useState<Date | null>(
|
||||||
const [selectedEntities, setSelectedEntities] = useState<string[]>([])
|
moment().add(1, "month").toDate()
|
||||||
const [isDownloading, setIsDownloading] = useState(false)
|
);
|
||||||
|
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;
|
||||||
@@ -96,75 +133,134 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
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))
|
||||||
|
|
||||||
const data: Item[] = useMemo(() =>
|
|
||||||
filteredAssignments.filter(a => selectedEntities.includes(a.entity || "")).flatMap(a => a.assignees.map(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
|
|
||||||
return { student, result, assignment: a, exams: assignmentExams, session, entity }
|
|
||||||
})).filter(x => !!x) as Item[],
|
|
||||||
[students, selectedEntities, filteredAssignments, exams, sessions, entities]
|
|
||||||
)
|
)
|
||||||
|
: assignments;
|
||||||
|
return endDate
|
||||||
|
? startDateFiltered.filter((a) =>
|
||||||
|
moment(a.endDate).isSameOrBefore(moment(endDate))
|
||||||
|
)
|
||||||
|
: startDateFiltered;
|
||||||
|
}, [startDate, endDate, assignments]);
|
||||||
|
|
||||||
const sortedData: Item[] = useMemo(() => data.sort((a, b) => {
|
const data: 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
|
filteredAssignments
|
||||||
|
.filter((a) => selectedEntities.includes(a.entity || ""))
|
||||||
|
.flatMap((a) =>
|
||||||
|
a.assignees.map((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
|
||||||
|
);
|
||||||
|
|
||||||
return bTotalScore - aTotalScore
|
if (!student) return undefined;
|
||||||
}), [data])
|
return {
|
||||||
|
student,
|
||||||
|
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 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;
|
||||||
|
}),
|
||||||
|
[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",
|
||||||
|
{
|
||||||
|
entities: entities.filter((e) => selectedEntities.includes(e.id)),
|
||||||
items: data,
|
items: data,
|
||||||
assignments: filteredAssignments,
|
assignments: filteredAssignments,
|
||||||
startDate,
|
startDate,
|
||||||
endDate
|
endDate,
|
||||||
}, {
|
},
|
||||||
responseType: 'blob'
|
{
|
||||||
})
|
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(
|
||||||
|
"download",
|
||||||
|
`statistical_${new Date().toISOString()}.xlsx`
|
||||||
|
);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
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", {
|
||||||
@@ -194,19 +290,26 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
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">
|
||||||
|
{capitalize(session.exam?.module || "")} Module, Part{" "}
|
||||||
|
{session.partIndex + 1}, Exercise {session.exerciseIndex + 1}
|
||||||
</span>
|
</span>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -224,13 +327,18 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
<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
|
||||||
|
href="/dashboard"
|
||||||
|
className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"
|
||||||
|
>
|
||||||
<BsChevronLeft />
|
<BsChevronLeft />
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-2xl">Statistical</h2>
|
<h2 className="font-bold text-2xl">Statistical</h2>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={value => setSelectedEntities(value ? mapBy(entities, 'id') : [])}
|
onChange={(value) =>
|
||||||
|
setSelectedEntities(value ? mapBy(entities, "id") : [])
|
||||||
|
}
|
||||||
isChecked={selectedEntities.length === entities.length}
|
isChecked={selectedEntities.length === entities.length}
|
||||||
>
|
>
|
||||||
Select All
|
Select All
|
||||||
@@ -241,13 +349,14 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
|
|
||||||
<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}
|
||||||
>
|
>
|
||||||
@@ -268,7 +377,7 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
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
|
||||||
@@ -278,13 +387,17 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
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
|
||||||
|
onClick={resetDateRange}
|
||||||
|
className="transition ease-in-out duration-300 rounded-full p-2 hover:bg-mti-gray-cool/10"
|
||||||
|
>
|
||||||
<BsX size={24} />
|
<BsX size={24} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-lg pr-1">
|
<span className="font-semibold text-lg pr-1">
|
||||||
Total: {totalAssignmentResolution.results} / {totalAssignmentResolution.total}
|
Total: {totalAssignmentResolution.results} /{" "}
|
||||||
|
{totalAssignmentResolution.total}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -293,13 +406,21 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={sortedData}
|
data={sortedData}
|
||||||
searchFields={[["student", "name"], ["student", "email"], ["student", "studentID"], ["exams", "id"], ["assignment", "name"]]}
|
searchFields={[
|
||||||
|
["student", "name"],
|
||||||
|
["student", "email"],
|
||||||
|
["student", "studentID"],
|
||||||
|
["exams", "id"],
|
||||||
|
["assignment", "name"],
|
||||||
|
]}
|
||||||
searchPlaceholder="Search by student, assignment or exam..."
|
searchPlaceholder="Search by student, assignment or exam..."
|
||||||
onDownload={entitiesAllowDownload.length > 0 ? downloadExcel : undefined}
|
onDownload={
|
||||||
|
entitiesAllowDownload.length > 0 ? downloadExcel : undefined
|
||||||
|
}
|
||||||
isDownloadLoading={isDownloading}
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -20,38 +20,55 @@ 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 entityIDs = mapBy(user.entities, "id");
|
||||||
const entities = await getEntitiesWithRoles(entityIDs)
|
const users = await getEntitiesUsers(entityIDs);
|
||||||
const users = await getEntitiesUsers(entityIDs)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({user, users, entities}),
|
props: serialize({ user, users, isAdmin }),
|
||||||
};
|
};
|
||||||
}, 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 } =
|
||||||
|
useFilterRecordsByUser<ITrainingContent[]>(
|
||||||
recordUserId || user?.id,
|
recordUserId || user?.id,
|
||||||
undefined,
|
undefined,
|
||||||
"training",
|
"training"
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +85,10 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
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`, {
|
||||||
|
userID: user.id,
|
||||||
|
stats: stats,
|
||||||
|
});
|
||||||
return response.data.id;
|
return response.data.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsNewContentLoading(false);
|
setIsNewContentLoading(false);
|
||||||
@@ -91,7 +111,9 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
router.push("/record");
|
router.push("/record");
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
|
const filterTrainingContentByDate = (trainingContent: {
|
||||||
|
[key: string]: ITrainingContent;
|
||||||
|
}) => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const filterDate = moment()
|
const filterDate = moment()
|
||||||
.subtract({ [filter as string]: 1 })
|
.subtract({ [filter as string]: 1 })
|
||||||
@@ -99,7 +121,8 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
const filteredTrainingContent: { [key: string]: ITrainingContent } = {};
|
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;
|
||||||
}
|
}
|
||||||
@@ -133,18 +156,22 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
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="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="flex flex-col md:gap-1 -md:gap-2 2xl:gap-2">
|
||||||
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
<span className="font-medium">{formatTimestamp(timestamp)}</span>
|
||||||
@@ -181,22 +208,33 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
<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...
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<RecordFilter entities={entities} user={user} filterState={{filter: filter, setFilter: setFilter}} assignments={false}>
|
<RecordFilter
|
||||||
|
entities={entities}
|
||||||
|
user={user}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
filterState={{ filter: filter, setFilter: setFilter }}
|
||||||
|
assignments={false}
|
||||||
|
>
|
||||||
{user.type === "student" && (
|
{user.type === "student" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="font-semibold text-2xl">Generate New Training Material</div>
|
<div className="font-semibold text-2xl">
|
||||||
|
Generate New Training Material
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4",
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light ml-4",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out"
|
||||||
)}
|
)}
|
||||||
onClick={handleNewTrainingContent}>
|
onClick={handleNewTrainingContent}
|
||||||
|
>
|
||||||
<FaPlus />
|
<FaPlus />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,12 +243,18 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
</RecordFilter>
|
</RecordFilter>
|
||||||
{trainingContent.length == 0 && (
|
{trainingContent.length == 0 && (
|
||||||
<div className="flex flex-grow justify-center items-center">
|
<div className="flex flex-grow justify-center items-center">
|
||||||
<span className="font-semibold ml-1">No training content to display...</span>
|
<span className="font-semibold ml-1">
|
||||||
|
No training content to display...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!areRecordsLoading && groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
{!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">
|
<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))
|
{Object.keys(
|
||||||
|
filterTrainingContentByDate(groupedByTrainingContent)
|
||||||
|
)
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
.map(trainingContentContainer)}
|
.map(trainingContentContainer)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,21 +17,26 @@ 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 }),
|
||||||
@@ -40,9 +45,9 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
|
|
||||||
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) => {
|
||||||
@@ -53,7 +58,10 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
|||||||
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 (
|
||||||
@@ -73,12 +81,15 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
|||||||
<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 />
|
<BsChevronLeft />
|
||||||
</button>
|
</button>
|
||||||
<h2 className="font-bold text-2xl">Student Performance ({students.length})</h2>
|
<h2 className="font-bold text-2xl">
|
||||||
|
Student Performance ({students.length})
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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