Compare commits
7 Commits
approval-w
...
vocabulary
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25aef3afdf | ||
|
|
df84aaadf4 | ||
|
|
2789660e8a | ||
|
|
6c7d189957 | ||
|
|
31f2a21a76 | ||
|
|
c49b1c8070 | ||
|
|
655e019bf6 |
@@ -114,5 +114,6 @@
|
|||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { BsArrowDown, BsArrowUp } from "react-icons/bs";
|
import { BsArrowDown, BsArrowUp } from "react-icons/bs";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
BsCurrencyDollar,
|
BsCurrencyDollar,
|
||||||
BsClipboardData,
|
BsClipboardData,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
|
BsChevronDown,
|
||||||
|
BsChevronUp,
|
||||||
|
BsChatText,
|
||||||
|
BsCardText,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { GoWorkflow } from "react-icons/go";
|
import { GoWorkflow } from "react-icons/go";
|
||||||
import { CiDumbbell } from "react-icons/ci";
|
import { CiDumbbell } from "react-icons/ci";
|
||||||
@@ -31,7 +35,7 @@ import {
|
|||||||
useAllowedEntities,
|
useAllowedEntities,
|
||||||
useAllowedEntitiesSomePermissions,
|
useAllowedEntitiesSomePermissions,
|
||||||
} from "@/hooks/useEntityPermissions";
|
} from "@/hooks/useEntityPermissions";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { PermissionType } from "../interfaces/permissions";
|
import { PermissionType } from "../interfaces/permissions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -52,6 +56,7 @@ interface NavProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isMinimized?: boolean;
|
isMinimized?: boolean;
|
||||||
badge?: number;
|
badge?: number;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Nav = ({
|
const Nav = ({
|
||||||
@@ -62,34 +67,71 @@ const Nav = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isMinimized = false,
|
isMinimized = false,
|
||||||
badge,
|
badge,
|
||||||
|
children,
|
||||||
}: NavProps) => {
|
}: NavProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Link
|
<div
|
||||||
href={!disabled ? keyPath : ""}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
|
"flex flex-col gap-2 transition-all duration-300 ease-in-out",
|
||||||
"transition-all duration-300 ease-in-out relative",
|
open && !isMinimized && "bg-white rounded-xl"
|
||||||
disabled
|
|
||||||
? "hover:bg-mti-gray-dim cursor-not-allowed"
|
|
||||||
: "hover:bg-mti-purple-light cursor-pointer",
|
|
||||||
path.startsWith(keyPath) && "bg-mti-purple-light text-white",
|
|
||||||
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon size={24} />
|
<Link
|
||||||
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
|
href={!disabled ? keyPath : ""}
|
||||||
{!!badge && badge > 0 && (
|
className={clsx(
|
||||||
<div
|
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
|
||||||
className={clsx(
|
"transition-all duration-300 ease-in-out relative",
|
||||||
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
|
disabled
|
||||||
"transition ease-in-out duration-300",
|
? "hover:bg-mti-gray-dim cursor-not-allowed"
|
||||||
isMinimized && "absolute right-0 top-0"
|
: "hover:bg-mti-purple-light cursor-pointer",
|
||||||
)}
|
path.startsWith(keyPath) && "bg-mti-purple-light text-white",
|
||||||
>
|
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]"
|
||||||
{badge}
|
)}
|
||||||
</div>
|
>
|
||||||
)}
|
<Icon size={24} />
|
||||||
</Link>
|
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
|
||||||
|
{!!badge && badge > 0 && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
|
||||||
|
"transition ease-in-out duration-300",
|
||||||
|
isMinimized && "absolute right-0 top-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children && (
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-4 rounded-full p-4 absolute right-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
setOpen((prev) => !prev);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<BsChevronUp
|
||||||
|
size={24}
|
||||||
|
className={clsx(
|
||||||
|
isMinimized && "hidden",
|
||||||
|
"transition ease-in-out duration-300"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BsChevronDown
|
||||||
|
size={24}
|
||||||
|
className={clsx(
|
||||||
|
isMinimized && "hidden",
|
||||||
|
"transition ease-in-out duration-300"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
{open || isMinimized ? children : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,7 +367,24 @@ export default function Sidebar({
|
|||||||
path={path}
|
path={path}
|
||||||
keyPath="/training"
|
keyPath="/training"
|
||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
>
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsChatText}
|
||||||
|
label="Vocabulary"
|
||||||
|
path={path}
|
||||||
|
keyPath="/training/vocabulary"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsCardText}
|
||||||
|
label="Grammar"
|
||||||
|
path={path}
|
||||||
|
keyPath="/training/grammar"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
|
</Nav>
|
||||||
)}
|
)}
|
||||||
{sidebarPermissions["viewPaymentRecords"] && (
|
{sidebarPermissions["viewPaymentRecords"] && (
|
||||||
<Nav
|
<Nav
|
||||||
@@ -424,7 +483,24 @@ export default function Sidebar({
|
|||||||
path={path}
|
path={path}
|
||||||
keyPath="/training"
|
keyPath="/training"
|
||||||
isMinimized
|
isMinimized
|
||||||
/>
|
>
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsChatText}
|
||||||
|
label="Vocabulary"
|
||||||
|
path={path}
|
||||||
|
keyPath="/training/vocabulary"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsCardText}
|
||||||
|
label="Grammar"
|
||||||
|
path={path}
|
||||||
|
keyPath="/training/grammar"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
|
</Nav>
|
||||||
)}
|
)}
|
||||||
{sidebarPermissions["viewPaymentRecords"] && (
|
{sidebarPermissions["viewPaymentRecords"] && (
|
||||||
<Nav
|
<Nav
|
||||||
|
|||||||
@@ -1,110 +1,158 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {Stat, StudentUser, User} from "@/interfaces/user";
|
import { Stat, StudentUser, User } from "@/interfaces/user";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {averageLevelCalculator} from "@/utils/score";
|
import { averageLevelCalculator } from "@/utils/score";
|
||||||
import {groupByExam} from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import {createColumnHelper} from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Table from "@/components/High/Table";
|
import Table from "@/components/High/Table";
|
||||||
|
|
||||||
type StudentPerformanceItem = StudentUser & {entitiesLabel: string; group: string};
|
type StudentPerformanceItem = StudentUser & {
|
||||||
|
entitiesLabel: string;
|
||||||
|
group: string;
|
||||||
|
userStats: Stat[];
|
||||||
|
};
|
||||||
|
|
||||||
const StudentPerformanceList = ({items = [], stats}: {items: StudentPerformanceItem[]; stats: Stat[]}) => {
|
const StudentPerformanceList = ({
|
||||||
const [isShowingAmount, setIsShowingAmount] = useState(false);
|
items = [],
|
||||||
|
}: {
|
||||||
|
items: StudentPerformanceItem[];
|
||||||
|
}) => {
|
||||||
|
const [isShowingAmount, setIsShowingAmount] = useState(false);
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<StudentPerformanceItem>();
|
const columnHelper = createColumnHelper<StudentPerformanceItem>();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: "Student Name",
|
header: "Student Name",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: "E-mail",
|
header: "E-mail",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("studentID", {
|
columnHelper.accessor("studentID", {
|
||||||
header: "ID",
|
header: "ID",
|
||||||
cell: (info) => info.getValue() || "N/A",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("group", {
|
columnHelper.accessor("group", {
|
||||||
header: "Group",
|
header: "Group",
|
||||||
cell: (info) => info.getValue(),
|
cell: (info) => info.getValue(),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("entitiesLabel", {
|
columnHelper.accessor("entitiesLabel", {
|
||||||
header: "Entities",
|
header: "Entities",
|
||||||
cell: (info) => info.getValue() || "N/A",
|
cell: (info) => info.getValue() || "N/A",
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("levels.reading", {
|
columnHelper.accessor("levels.reading", {
|
||||||
header: "Reading",
|
header: "Reading",
|
||||||
cell: (info) =>
|
cell: (info) =>
|
||||||
!isShowingAmount
|
!isShowingAmount
|
||||||
? info.getValue() || 0
|
? info.getValue() || 0
|
||||||
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
|
: `${
|
||||||
}),
|
Object.keys(
|
||||||
columnHelper.accessor("levels.listening", {
|
groupByExam(
|
||||||
header: "Listening",
|
info.row.original.userStats.filter(
|
||||||
cell: (info) =>
|
(x) => x.module === "reading"
|
||||||
!isShowingAmount
|
)
|
||||||
? info.getValue() || 0
|
)
|
||||||
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
|
).length
|
||||||
}),
|
} exams`,
|
||||||
columnHelper.accessor("levels.writing", {
|
}),
|
||||||
header: "Writing",
|
columnHelper.accessor("levels.listening", {
|
||||||
cell: (info) =>
|
header: "Listening",
|
||||||
!isShowingAmount
|
cell: (info) =>
|
||||||
? info.getValue() || 0
|
!isShowingAmount
|
||||||
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
|
? info.getValue() || 0
|
||||||
}),
|
: `${
|
||||||
columnHelper.accessor("levels.speaking", {
|
Object.keys(
|
||||||
header: "Speaking",
|
groupByExam(
|
||||||
cell: (info) =>
|
info.row.original.userStats.filter(
|
||||||
!isShowingAmount
|
(x) => x.module === "listening"
|
||||||
? info.getValue() || 0
|
)
|
||||||
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
|
)
|
||||||
}),
|
).length
|
||||||
columnHelper.accessor("levels.level", {
|
} exams`,
|
||||||
header: "Level",
|
}),
|
||||||
cell: (info) =>
|
columnHelper.accessor("levels.writing", {
|
||||||
!isShowingAmount
|
header: "Writing",
|
||||||
? info.getValue() || 0
|
cell: (info) =>
|
||||||
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
|
!isShowingAmount
|
||||||
}),
|
? info.getValue() || 0
|
||||||
columnHelper.accessor("levels", {
|
: `${
|
||||||
id: "overall_level",
|
Object.keys(
|
||||||
header: "Overall",
|
groupByExam(
|
||||||
cell: (info) =>
|
info.row.original.userStats.filter(
|
||||||
!isShowingAmount
|
(x) => x.module === "writing"
|
||||||
? averageLevelCalculator(
|
)
|
||||||
items,
|
)
|
||||||
stats.filter((x) => x.user === info.row.original.id),
|
).length
|
||||||
).toFixed(1)
|
} exams`,
|
||||||
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
|
}),
|
||||||
}),
|
columnHelper.accessor("levels.speaking", {
|
||||||
];
|
header: "Speaking",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${
|
||||||
|
Object.keys(
|
||||||
|
groupByExam(
|
||||||
|
info.row.original.userStats.filter(
|
||||||
|
(x) => x.module === "speaking"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).length
|
||||||
|
} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("levels.level", {
|
||||||
|
header: "Level",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? info.getValue() || 0
|
||||||
|
: `${
|
||||||
|
Object.keys(
|
||||||
|
groupByExam(
|
||||||
|
info.row.original.userStats.filter(
|
||||||
|
(x) => x.module === "level"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).length
|
||||||
|
} exams`,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("userStats", {
|
||||||
|
id: "overall_level",
|
||||||
|
header: "Overall",
|
||||||
|
cell: (info) =>
|
||||||
|
!isShowingAmount
|
||||||
|
? averageLevelCalculator(
|
||||||
|
info.row.original.focus,
|
||||||
|
info.getValue()
|
||||||
|
).toFixed(1)
|
||||||
|
: `${Object.keys(groupByExam(info.getValue())).length} exams`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full h-full">
|
<div className="flex flex-col gap-4 w-full h-full">
|
||||||
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
|
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
|
||||||
Show Utilization
|
Show Utilization
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Table<StudentPerformanceItem>
|
<Table<StudentPerformanceItem>
|
||||||
data={items.sort(
|
data={items.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
averageLevelCalculator(
|
averageLevelCalculator(b.focus, b.userStats) -
|
||||||
items,
|
averageLevelCalculator(a.focus, a.userStats)
|
||||||
stats.filter((x) => x.user === b.id),
|
)}
|
||||||
) -
|
columns={columns}
|
||||||
averageLevelCalculator(
|
searchFields={[
|
||||||
items,
|
["name"],
|
||||||
stats.filter((x) => x.user === a.id),
|
["email"],
|
||||||
),
|
["studentID"],
|
||||||
)}
|
["entitiesLabel"],
|
||||||
columns={columns}
|
["group"],
|
||||||
searchFields={[["name"], ["email"], ["studentID"], ["entitiesLabel"], ["group"]]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StudentPerformanceList;
|
export default StudentPerformanceList;
|
||||||
|
|||||||
@@ -48,4 +48,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: true });
|
await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: true });
|
||||||
|
|
||||||
res.status(200).json({ ok: true });
|
res.status(200).json({ ok: true });
|
||||||
|
const sessions = await db.collection("sessions").find<Session>({ user: session.user }, { projection: { id: 1 } }).sort({ date: 1 }).toArray();
|
||||||
|
// Delete old sessions
|
||||||
|
if (sessions.length > 5) {
|
||||||
|
await db.collection("sessions").deleteOne({ id: { $in: sessions.slice(0, sessions.length - 5).map(x => x.id) } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default function Home({ user, users }: Props) {
|
|||||||
const [licenses, setLicenses] = useState(0);
|
const [licenses, setLicenses] = useState(0);
|
||||||
|
|
||||||
const { rows, renderSearch } = useListSearch<User>(
|
const { rows, renderSearch } = useListSearch<User>(
|
||||||
[["name"], ["corporateInformation", "companyInformation", "name"]],
|
[["name"], ["email"], ["corporateInformation", "companyInformation", "name"]],
|
||||||
users
|
users
|
||||||
);
|
);
|
||||||
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
const { items, renderMinimal } = usePagination<User>(rows, 16);
|
||||||
|
|||||||
41
src/pages/training/grammar.tsx
Normal file
41
src/pages/training/grammar.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Head from "next/head";
|
||||||
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { User } from "@/interfaces/user";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { redirect, serialize } from "@/utils";
|
||||||
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
|
const user = await requestUser(req, res);
|
||||||
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: serialize({ user }),
|
||||||
|
};
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
const Grammar: React.FC<{
|
||||||
|
user: User;
|
||||||
|
}> = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Training | EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Grammar;
|
||||||
41
src/pages/training/vocabulary.tsx
Normal file
41
src/pages/training/vocabulary.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
import Head from "next/head";
|
||||||
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { User } from "@/interfaces/user";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
|
import { redirect, serialize } from "@/utils";
|
||||||
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
|
const user = await requestUser(req, res);
|
||||||
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: serialize({ user }),
|
||||||
|
};
|
||||||
|
}, sessionOptions);
|
||||||
|
|
||||||
|
const Vocabulary: React.FC<{
|
||||||
|
user: User;
|
||||||
|
}> = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Training | EnCoach</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Vocabulary;
|
||||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { BsChevronLeft } from "react-icons/bs";
|
import { BsChevronLeft } from "react-icons/bs";
|
||||||
import { mapBy, serialize } from "@/utils";
|
import { mapBy, serialize } from "@/utils";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
import { getUsersWithStats } from "@/utils/users.be";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
@@ -33,8 +33,32 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
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" })
|
? getUsersWithStats(
|
||||||
: getEntitiesUsers(mapBy(allowedEntities, "id"), { type: "student" }));
|
{ type: "student" },
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
entities: 1,
|
||||||
|
focus: 1,
|
||||||
|
email: 1,
|
||||||
|
name: 1,
|
||||||
|
levels: 1,
|
||||||
|
userStats: 1,
|
||||||
|
studentID: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: getUsersWithStats(
|
||||||
|
{ type: "student", "entities.id": { in: mapBy(entities, "id") } },
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
entities: 1,
|
||||||
|
focus: 1,
|
||||||
|
email: 1,
|
||||||
|
name: 1,
|
||||||
|
levels: 1,
|
||||||
|
userStats: 1,
|
||||||
|
studentID: 1,
|
||||||
|
}
|
||||||
|
));
|
||||||
const groups = await getParticipantsGroups(mapBy(students, "id"));
|
const groups = await getParticipantsGroups(mapBy(students, "id"));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -44,14 +68,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: StudentUser[];
|
students: (StudentUser & { userStats: Stat[] })[];
|
||||||
entities: Entity[];
|
entities: Entity[];
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
const StudentPerformance = ({ students, entities, groups }: Props) => {
|
||||||
const { data: stats } = useFilterRecordsByUser<Stat[]>();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const performanceStudents = students.map((u) => ({
|
const performanceStudents = students.map((u) => ({
|
||||||
@@ -76,7 +98,6 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -91,7 +112,7 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
|||||||
Student Performance ({students.length})
|
Student Performance ({students.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
<StudentPerformanceList items={performanceStudents} />
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import {
|
import {
|
||||||
Exam,
|
Exam,
|
||||||
ReadingExam,
|
ReadingExam,
|
||||||
@@ -70,9 +70,9 @@ export const getExamById = async (module: Module, id: string): Promise<Exam | un
|
|||||||
|
|
||||||
export const defaultExamUserSolutions = (exam: Exam) => {
|
export const defaultExamUserSolutions = (exam: Exam) => {
|
||||||
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level")
|
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level")
|
||||||
return exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam));
|
return (exam.parts.flatMap((x) => x.exercises) ?? []).map((x) => defaultUserSolutions(x, exam));
|
||||||
|
|
||||||
return exam.exercises.map((x) => defaultUserSolutions(x, exam));
|
return (exam.exercises ?? []).map((x) => defaultUserSolutions(x, exam));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSolution => {
|
export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSolution => {
|
||||||
@@ -88,26 +88,26 @@ export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSoluti
|
|||||||
switch (exercise.type) {
|
switch (exercise.type) {
|
||||||
case "fillBlanks":
|
case "fillBlanks":
|
||||||
total = exercise.text.match(/({{\d+}})/g)?.length || 0;
|
total = exercise.text.match(/({{\d+}})/g)?.length || 0;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
case "matchSentences":
|
case "matchSentences":
|
||||||
total = exercise.sentences.length;
|
total = exercise.sentences.length;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
case "multipleChoice":
|
case "multipleChoice":
|
||||||
total = exercise.questions.length;
|
total = exercise.questions.length;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
case "writeBlanks":
|
case "writeBlanks":
|
||||||
total = exercise.text.match(/({{\d+}})/g)?.length || 0;
|
total = exercise.text.match(/({{\d+}})/g)?.length || 0;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
case "trueFalse":
|
case "trueFalse":
|
||||||
total = exercise.questions.length;
|
total = exercise.questions.length;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
case "writing":
|
case "writing":
|
||||||
total = 1;
|
total = 1;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
case "speaking":
|
case "speaking":
|
||||||
total = 1;
|
total = 1;
|
||||||
return {...defaultSettings, score: {correct: 0, total, missing: total}};
|
return { ...defaultSettings, score: { correct: 0, total, missing: total } };
|
||||||
default:
|
default:
|
||||||
return {...defaultSettings, score: {correct: 0, total: 0, missing: 0}};
|
return { ...defaultSettings, score: { correct: 0, total: 0, missing: 0 } };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {Module, Step} from "@/interfaces";
|
import { Module, Step } from "@/interfaces";
|
||||||
import {Stat, User} from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
|
|
||||||
type Type = "academic" | "general";
|
type Type = "academic" | "general";
|
||||||
|
|
||||||
export const writingReverseMarking: {[key: number]: number} = {
|
export const writingReverseMarking: { [key: number]: number } = {
|
||||||
9: 90,
|
9: 90,
|
||||||
8.5: 85,
|
8.5: 85,
|
||||||
8: 80,
|
8: 80,
|
||||||
@@ -25,7 +25,7 @@ export const writingReverseMarking: {[key: number]: number} = {
|
|||||||
0: 0,
|
0: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const speakingReverseMarking: {[key: number]: number} = {
|
export const speakingReverseMarking: { [key: number]: number } = {
|
||||||
9: 90,
|
9: 90,
|
||||||
8.5: 85,
|
8.5: 85,
|
||||||
8: 80,
|
8: 80,
|
||||||
@@ -47,7 +47,7 @@ export const speakingReverseMarking: {[key: number]: number} = {
|
|||||||
0: 0,
|
0: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writingMarking: {[key: number]: number} = {
|
export const writingMarking: { [key: number]: number } = {
|
||||||
90: 9,
|
90: 9,
|
||||||
80: 8,
|
80: 8,
|
||||||
70: 7,
|
70: 7,
|
||||||
@@ -60,7 +60,7 @@ export const writingMarking: {[key: number]: number} = {
|
|||||||
0: 0,
|
0: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const readingGeneralMarking: {[key: number]: number} = {
|
const readingGeneralMarking: { [key: number]: number } = {
|
||||||
100: 9,
|
100: 9,
|
||||||
97.5: 8.5,
|
97.5: 8.5,
|
||||||
92.5: 8,
|
92.5: 8,
|
||||||
@@ -77,7 +77,7 @@ const readingGeneralMarking: {[key: number]: number} = {
|
|||||||
15: 2.5,
|
15: 2.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const academicMarking: {[key: number]: number} = {
|
const academicMarking: { [key: number]: number } = {
|
||||||
97.5: 9,
|
97.5: 9,
|
||||||
92.5: 8.5,
|
92.5: 8.5,
|
||||||
87.5: 8,
|
87.5: 8,
|
||||||
@@ -94,7 +94,7 @@ const academicMarking: {[key: number]: number} = {
|
|||||||
10: 2.5,
|
10: 2.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const levelMarking: {[key: number]: number} = {
|
const levelMarking: { [key: number]: number } = {
|
||||||
88: 9, // Advanced
|
88: 9, // Advanced
|
||||||
64: 8, // Upper-Intermediate
|
64: 8, // Upper-Intermediate
|
||||||
52: 6, // Intermediate
|
52: 6, // Intermediate
|
||||||
@@ -103,7 +103,7 @@ const levelMarking: {[key: number]: number} = {
|
|||||||
0: 0, // Beginner
|
0: 0, // Beginner
|
||||||
};
|
};
|
||||||
|
|
||||||
const moduleMarkings: {[key in Module | "overall"]: {[key in Type]: {[key: number]: number}}} = {
|
const moduleMarkings: { [key in Module | "overall"]: { [key in Type]: { [key: number]: number } } } = {
|
||||||
reading: {
|
reading: {
|
||||||
academic: academicMarking,
|
academic: academicMarking,
|
||||||
general: readingGeneralMarking,
|
general: readingGeneralMarking,
|
||||||
@@ -147,7 +147,7 @@ export const calculateBandScore = (correct: number, total: number, module: Modul
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateAverageLevel = (levels: {[key in Module]: number}) => {
|
export const calculateAverageLevel = (levels: { [key in Module]: number }) => {
|
||||||
return (
|
return (
|
||||||
Object.keys(levels)
|
Object.keys(levels)
|
||||||
.filter((x) => x !== "level")
|
.filter((x) => x !== "level")
|
||||||
@@ -193,20 +193,21 @@ export const getGradingLabel = (score: number, grading: Step[]) => {
|
|||||||
return "N/A";
|
return "N/A";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const averageLevelCalculator = (users: User[], studentStats: Stat[]) => {
|
export const averageLevelCalculator = (focus: Type, studentStats: Stat[]) => {
|
||||||
const formattedStats = studentStats
|
/* const formattedStats = studentStats
|
||||||
.map((s) => ({
|
.map((s) => ({
|
||||||
focus: users.find((u) => u.id === s.user)?.focus,
|
focus: focus,
|
||||||
score: s.score,
|
score: s.score,
|
||||||
module: s.module,
|
module: s.module,
|
||||||
}))
|
}))
|
||||||
.filter((f) => !!f.focus);
|
.filter((f) => !!f.focus); */
|
||||||
const bandScores = formattedStats.map((s) => ({
|
|
||||||
|
const bandScores = studentStats.map((s) => ({
|
||||||
module: s.module,
|
module: s.module,
|
||||||
level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!),
|
level: calculateBandScore(s.score.correct, s.score.total, s.module, focus),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const levels: {[key in Module]: number} = {
|
const levels: { [key in Module]: number } = {
|
||||||
reading: 0,
|
reading: 0,
|
||||||
listening: 0,
|
listening: 0,
|
||||||
writing: 0,
|
writing: 0,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import client from "@/lib/mongodb";
|
|||||||
import { EntityWithRoles, WithEntities } from "@/interfaces/entity";
|
import { EntityWithRoles, WithEntities } from "@/interfaces/entity";
|
||||||
import { getEntity } from "./entities.be";
|
import { getEntity } from "./entities.be";
|
||||||
import { getRole } from "./roles.be";
|
import { getRole } from "./roles.be";
|
||||||
import { groupAllowedEntitiesByPermissions } from "./permissions";
|
import { groupAllowedEntitiesByPermissions } from "./permissions";
|
||||||
import { mapBy } from ".";
|
import { mapBy } from ".";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
@@ -20,6 +20,15 @@ export async function getUsers(filter?: object, limit = 0, sort = {}, projection
|
|||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUsersWithStats(filter?: object, projection = {}, limit = 0, sort = {}) {
|
||||||
|
return await db
|
||||||
|
.collection("usersWithStats")
|
||||||
|
.find<User>(filter || {}, { projection: { _id: 0, ...projection } })
|
||||||
|
.limit(limit)
|
||||||
|
.sort(sort)
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
export async function searchUsers(searchInput?: string, limit = 50, page = 0, sort: object = { "name": 1 }, projection = {}, filter?: object) {
|
export async function searchUsers(searchInput?: string, limit = 50, page = 0, sort: object = { "name": 1 }, projection = {}, filter?: object) {
|
||||||
const compoundFilter = {
|
const compoundFilter = {
|
||||||
"compound": {
|
"compound": {
|
||||||
|
|||||||
Reference in New Issue
Block a user