Compare commits

...

12 Commits

Author SHA1 Message Date
Tiago Ribeiro
2bfb94d01b Merged in develop (pull request #162)
Implemented limit 5 sessions per User
2025-03-05 08:22:28 +00:00
Francisco Lima
df84aaadf4 Merged in limit5SessionsUser (pull request #161)
Implemented limit 5 sessions per User

Approved-by: Tiago Ribeiro
2025-03-05 08:17:05 +00:00
José Lima
2789660e8a Implemented limit 5 sessions per User 2025-03-05 04:42:54 +00:00
Tiago Ribeiro
a78e6eb64f Merged in develop (pull request #160)
Develop
2025-03-04 23:24:59 +00:00
Francisco Lima
6c7d189957 Merged in fixStudentPerformanceFreeze (pull request #159)
FixStudentPerformanceFreeze

Approved-by: Tiago Ribeiro
2025-03-04 23:24:17 +00:00
José Lima
31f2a21a76 reverted unnecessary changes 2025-03-04 23:17:20 +00:00
José Lima
c49b1c8070 Fix student performance freeze and search users in create entities
TODO: pagination in student performance freeze
2025-03-04 23:12:26 +00:00
Tiago Ribeiro
d78654a30f Merged in develop (pull request #158)
Develop
2025-03-04 10:02:57 +00:00
João Correia
655e019bf6 Merged in approval-workflows (pull request #157)
add approved field to exam

Approved-by: Tiago Ribeiro
2025-03-04 01:44:04 +00:00
Tiago Ribeiro
d7a8f496c0 Merged develop into approval-workflows 2025-03-04 01:43:32 +00:00
João Correia
d77336374d Merged in approval-workflows (pull request #156)
Approval workflows

Approved-by: Tiago Ribeiro
2025-03-03 11:17:40 +00:00
Tiago Ribeiro
53d6b0dd51 Merged in develop (pull request #153)
Prod Update - 12/02/2025
2025-02-12 09:13:08 +00:00
9 changed files with 225 additions and 140 deletions

View File

@@ -114,5 +114,6 @@
"husky": "^8.0.3",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.4"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -9,7 +9,7 @@ import {
useReactTable,
} from "@tanstack/react-table";
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useState } from "react";
import { BsArrowDown, BsArrowUp } from "react-icons/bs";
import Button from "../Low/Button";

View File

@@ -7,9 +7,17 @@ import {createColumnHelper} from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox";
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 = ({
items = [],
}: {
items: StudentPerformanceItem[];
}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const columnHelper = createColumnHelper<StudentPerformanceItem>();
@@ -40,46 +48,86 @@ const StudentPerformanceList = ({items = [], stats}: {items: StudentPerformanceI
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "reading"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "listening"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "writing"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
: `${
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(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "level"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels", {
columnHelper.accessor("userStats", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
items,
stats.filter((x) => x.user === info.row.original.id),
info.row.original.focus,
info.getValue()
).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
: `${Object.keys(groupByExam(info.getValue())).length} exams`,
}),
];
@@ -91,17 +139,17 @@ const StudentPerformanceList = ({items = [], stats}: {items: StudentPerformanceI
<Table<StudentPerformanceItem>
data={items.sort(
(a, b) =>
averageLevelCalculator(
items,
stats.filter((x) => x.user === b.id),
) -
averageLevelCalculator(
items,
stats.filter((x) => x.user === a.id),
),
averageLevelCalculator(b.focus, b.userStats) -
averageLevelCalculator(a.focus, a.userStats)
)}
columns={columns}
searchFields={[["name"], ["email"], ["studentID"], ["entitiesLabel"], ["group"]]}
searchFields={[
["name"],
["email"],
["studentID"],
["entitiesLabel"],
["group"],
]}
/>
</div>
);

View File

@@ -48,4 +48,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: 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) } });
}
}

View File

@@ -70,7 +70,7 @@ export default function Home({ user, users }: Props) {
const [licenses, setLicenses] = useState(0);
const { rows, renderSearch } = useListSearch<User>(
[["name"], ["corporateInformation", "companyInformation", "name"]],
[["name"], ["email"], ["corporateInformation", "companyInformation", "name"]],
users
);
const { items, renderMinimal } = usePagination<User>(rows, 16);

View File

@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { BsChevronLeft } from "react-icons/bs";
import { mapBy, serialize } from "@/utils";
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 { checkAccess, findAllowedEntities } from "@/utils/permissions";
import { getEntitiesWithRoles } from "@/utils/entities.be";
@@ -33,8 +33,32 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
if (allowedEntities.length === 0) return redirect("/");
const students = await (checkAccess(user, ["admin", "developer"])
? getUsers({ type: "student" })
: getEntitiesUsers(mapBy(allowedEntities, "id"), { type: "student" }));
? getUsersWithStats(
{ 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"));
return {
@@ -44,14 +68,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
interface Props {
user: User;
students: StudentUser[];
students: (StudentUser & { userStats: Stat[] })[];
entities: Entity[];
groups: Group[];
}
const StudentPerformance = ({ user, students, entities, groups }: Props) => {
const { data: stats } = useFilterRecordsByUser<Stat[]>();
const StudentPerformance = ({ students, entities, groups }: Props) => {
const router = useRouter();
const performanceStudents = students.map((u) => ({
@@ -76,7 +98,6 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
<>
<div className="flex items-center gap-2">
<button
@@ -91,7 +112,7 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
Student Performance ({students.length})
</h2>
</div>
<StudentPerformanceList items={performanceStudents} stats={stats} />
<StudentPerformanceList items={performanceStudents} />
</>
</>
);

View File

@@ -70,9 +70,9 @@ export const getExamById = async (module: Module, id: string): Promise<Exam | un
export const defaultExamUserSolutions = (exam: Exam) => {
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 => {

View File

@@ -193,17 +193,18 @@ export const getGradingLabel = (score: number, grading: Step[]) => {
return "N/A";
};
export const averageLevelCalculator = (users: User[], studentStats: Stat[]) => {
const formattedStats = studentStats
export const averageLevelCalculator = (focus: Type, studentStats: Stat[]) => {
/* const formattedStats = studentStats
.map((s) => ({
focus: users.find((u) => u.id === s.user)?.focus,
focus: focus,
score: s.score,
module: s.module,
}))
.filter((f) => !!f.focus);
const bandScores = formattedStats.map((s) => ({
.filter((f) => !!f.focus); */
const bandScores = studentStats.map((s) => ({
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 } = {

View File

@@ -20,6 +20,15 @@ export async function getUsers(filter?: object, limit = 0, sort = {}, projection
.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) {
const compoundFilter = {
"compound": {