From 770056e0c4ed3875e3969a7e63afa75d56bd0172 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 24 Dec 2024 10:31:52 +0000 Subject: [PATCH] Improved part of the performance of the dashboards --- src/interfaces/user.ts | 1 + src/pages/(exam)/ExamPage.tsx | 1 - src/pages/api/stats/update.ts | 48 +++++++++++++------------ src/pages/dashboard/corporate.tsx | 36 +++++++++++-------- src/pages/dashboard/developer.tsx | 14 ++++---- src/pages/dashboard/mastercorporate.tsx | 36 +++++++++++-------- src/pages/exam.tsx | 2 +- src/utils/entities.be.ts | 43 +++++++++++++--------- src/utils/roles.be.ts | 14 ++++---- 9 files changed, 110 insertions(+), 85 deletions(-) diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 8de4bf56..b1ed48bc 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -28,6 +28,7 @@ export interface BasicUser { export interface StudentUser extends BasicUser { type: "student"; studentID?: string; + averageLevel?: number preferredGender?: InstructorGender; demographicInformation?: DemographicInformation; preferredTopics?: string[]; diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 9a041a35..5ca4ef24 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -60,7 +60,6 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = setFlags, setShuffles, evaluated, - setEvaluated, } = useExamStore(); const [isFetchingExams, setIsFetchingExams] = useState(false); diff --git a/src/pages/api/stats/update.ts b/src/pages/api/stats/update.ts index f3d27fbb..eeff1855 100644 --- a/src/pages/api/stats/update.ts +++ b/src/pages/api/stats/update.ts @@ -1,15 +1,15 @@ -import {MODULES} from "@/constants/ielts"; -import {app} from "@/firebase"; -import {Module} from "@/interfaces"; -import {Stat, User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import {calculateBandScore} from "@/utils/score"; -import {groupByModule, groupBySession} from "@/utils/stats"; +import { MODULES } from "@/constants/ielts"; +import { app } from "@/firebase"; +import { Module } from "@/interfaces"; +import { Stat, User } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; +import { groupByModule, groupBySession } from "@/utils/stats"; import { MODULE_ARRAY } from "@/utils/moduleUtils"; import client from "@/lib/mongodb"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {groupBy} from "lodash"; -import {NextApiRequest, NextApiResponse} from "next"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { groupBy } from "lodash"; +import { NextApiRequest, NextApiResponse } from "next"; import { requestUser } from "@/utils/api"; const db = client.db(process.env.MONGODB_DB); @@ -29,8 +29,8 @@ async function update(req: NextApiRequest, res: NextApiResponse) { const stats = await db.collection("stats").find({ user: user.id }).toArray(); const groupedStats = groupBySession(stats); - const sessionLevels: {[key in Module]: {correct: number; total: number}}[] = Object.keys(groupedStats).map((key) => { - const sessionStats = groupedStats[key].map((stat) => ({module: stat.module, correct: stat.score.correct, total: stat.score.total})); + const sessionLevels: { [key in Module]: { correct: number; total: number } }[] = Object.keys(groupedStats).map((key) => { + const sessionStats = groupedStats[key].map((stat) => ({ module: stat.module, correct: stat.score.correct, total: stat.score.total })); const sessionLevels = { reading: { correct: 0, @@ -59,8 +59,8 @@ async function update(req: NextApiRequest, res: NextApiResponse) { if (moduleStats.length === 0) return; const moduleScore = moduleStats.reduce( - (accumulator, current) => ({correct: accumulator.correct + current.correct, total: accumulator.total + current.total}), - {correct: 0, total: 0}, + (accumulator, current) => ({ correct: accumulator.correct + current.correct, total: accumulator.total + current.total }), + { correct: 0, total: 0 }, ); sessionLevels[module] = moduleScore; @@ -72,24 +72,24 @@ async function update(req: NextApiRequest, res: NextApiResponse) { const readingLevel = sessionLevels .map((x) => x.reading) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const listeningLevel = sessionLevels .map((x) => x.listening) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const writingLevel = sessionLevels .map((x) => x.writing) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const speakingLevel = sessionLevels .map((x) => x.speaking) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); - const levelLevel = sessionLevels + const levelLevel = sessionLevels .map((x) => x.level) .filter((x) => x.total > 0) - .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + .reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 }); const levels = { @@ -100,12 +100,14 @@ async function update(req: NextApiRequest, res: NextApiResponse) { level: calculateBandScore(levelLevel.correct, levelLevel.total, "level", user.focus), }; + const averageLevel = calculateAverageLevel(levels) + await db.collection("users").updateOne( - { id: user.id}, - { $set: {levels} } + { id: user.id }, + { $set: { levels, averageLevel } } ); - res.status(200).json({ok: true}); + res.status(200).json({ ok: true }); } else { res.status(401).json(undefined); } diff --git a/src/pages/dashboard/corporate.tsx b/src/pages/dashboard/corporate.tsx index 2504646a..3eb71ca5 100644 --- a/src/pages/dashboard/corporate.tsx +++ b/src/pages/dashboard/corporate.tsx @@ -3,18 +3,18 @@ import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; import { EntityWithRoles } from "@/interfaces/entity"; -import { Stat, Type, User } from "@/interfaces/user"; +import { Stat, StudentUser, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { countEntitiesAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; import { countGroupsByEntities } from "@/utils/groups.be"; -import { checkAccess } from "@/utils/permissions"; +import { checkAccess, findAllowedEntities } from "@/utils/permissions"; import { calculateAverageLevel } from "@/utils/score"; import { groupByExam } from "@/utils/stats"; import { getStatsByUsers } from "@/utils/stats.be"; -import { countAllowedUsers, filterAllowedUsers } from "@/utils/users.be"; +import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; import { uniqBy } from "lodash"; import moment from "moment"; @@ -37,7 +37,9 @@ import { isAdmin } from "@/utils/users"; interface Props { user: User; - users: User[]; + students: StudentUser[] + latestStudents: User[] + latestTeachers: User[] userCounts: { [key in Type]: number } entities: EntityWithRoles[]; assignmentsCount: number; @@ -53,21 +55,25 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const entityIDS = mapBy(user.entities, "id") || []; const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); - const users = await filterAllowedUsers(user, entities) + + const allowedStudentEntities = findAllowedEntities(user, entities, "view_students") + const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers") + + const students = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 }); + const latestStudents = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { registrationDate: -1 }) + const latestTeachers = + await getUsers({ type: 'teacher', "entities.id": { $in: mapBy(allowedTeacherEntities, 'id') } }, 10, { registrationDate: -1 }) const userCounts = await countAllowedUsers(user, entities) const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } }); const groupsCount = await countGroupsByEntities(mapBy(entities, "id")); - const stats = await getStatsByUsers(users.map((u) => u.id)); - - return { props: serialize({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }) }; + return { props: serialize({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, groupsCount }) }; }, sessionOptions); -export default function Dashboard({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]); - +export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) { const totalCount = useMemo(() => userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts]) const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities]) @@ -159,15 +165,15 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen
dateSorter(a, b, "desc", "registrationDate"))} + users={latestStudents} title="Latest Students" /> dateSorter(a, b, "desc", "registrationDate"))} + users={latestTeachers} title="Latest Teachers" /> calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + users={students} title="Highest level students" /> { if (!checkAccess(user, ["admin", "developer"])) return redirect("/") - const students = await getUsers({ type: 'student' }); + const students = await getUsers({ type: 'student' }, 10, { averageLevel: -1 }); const usersCount = { student: await countUsers({ type: "student" }), teacher: await countUsers({ type: "teacher" }), @@ -66,20 +66,18 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const assignmentsCount = await countEntitiesAssignments(mapBy(entities, 'id'), { archived: { $ne: true } }); const groupsCount = await countGroups(); - const stats = await getStatsByUsers(mapBy(students, 'id')); - - return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, stats, groupsCount }) }; + return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, groupsCount }) }; }, sessionOptions); export default function Dashboard({ user, - students, + students = [], latestStudents, latestTeachers, usersCount, entities, assignmentsCount, - stats, + stats = [], groupsCount }: Props) { const router = useRouter(); @@ -170,7 +168,7 @@ export default function Dashboard({ title="Latest Teachers" /> calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + users={students} title="Highest level students" /> { const user = await requestUser(req, res) if (!user || !user.isVerified) return redirect("/login") - if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) - return redirect("/") + if (!checkAccess(user, ["admin", "developer", "corporate"])) return redirect("/") const entityIDS = mapBy(user.entities, "id") || []; const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); - const users = await filterAllowedUsers(user, entities) + + const allowedStudentEntities = findAllowedEntities(user, entities, "view_students") + const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers") + + const students = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 }); + const latestStudents = + await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { registrationDate: -1 }) + const latestTeachers = + await getUsers({ type: 'teacher', "entities.id": { $in: mapBy(allowedTeacherEntities, 'id') } }, 10, { registrationDate: -1 }) const userCounts = await countAllowedUsers(user, entities) const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } }); const groupsCount = await countGroupsByEntities(mapBy(entities, "id")); - const stats = await getStatsByUsers(users.map((u) => u.id)); - - return { props: serialize({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }) }; + return { props: serialize({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, groupsCount }) }; }, sessionOptions); -export default function Dashboard({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]); +export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) { const totalCount = useMemo(() => userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts]) @@ -168,15 +174,15 @@ export default function Dashboard({ user, users, userCounts, entities, assignmen
dateSorter(a, b, "desc", "registrationDate"))} + users={latestStudents} title="Latest Students" /> dateSorter(a, b, "desc", "registrationDate"))} + users={latestTeachers} title="Latest Teachers" /> calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + users={students} title="Highest level students" /> { - return (await db.collection("entities").findOne({id})) ?? undefined; + return (await db.collection("entities").findOne({ id })) ?? undefined; }; export const getEntitiesWithRoles = async (ids?: string[]): Promise => { const entities = await db .collection("entities") - .find(ids ? {id: {$in: ids}} : {}) + .find(ids ? { id: { $in: ids } } : {}) .toArray(); const roles = await getRolesByEntities(entities.map((x) => x.id)); - return entities.map((x) => ({...x, roles: roles.filter((y) => y.entityID === x.id) || []})); + return entities.map((x) => ({ ...x, roles: roles.filter((y) => y.entityID === x.id) || [] })); }; export const getEntities = async (ids?: string[]) => { return await db .collection("entities") - .find(ids ? {id: {$in: ids}} : {}) + .find(ids ? { id: { $in: ids } } : {}) .toArray(); }; @@ -57,41 +57,52 @@ export const createEntity = async (entity: Entity) => { await db.collection("roles").insertOne(defaultRole) await db.collection("roles").insertOne(adminRole) - return {default: defaultRole, admin: adminRole} + return { default: defaultRole, admin: adminRole } } export const addUserToEntity = async (user: string, entity: string, role: string) => await db.collection("users").updateOne( - {id: user}, + { id: user }, { // @ts-expect-error $push: { - entities: {id: entity, role}, + entities: { id: entity, role }, }, }, ); export const addUsersToEntity = async (users: string[], entity: string, role: string) => await db.collection("users").updateMany( - {id: {$in: users}}, + { id: { $in: users } }, { // @ts-expect-error $push: { - entities: {id: entity, role}, + entities: { id: entity, role }, + }, + }, + ); + +export const removeUsersFromEntity = async (users: string[], entity: string) => + await db.collection("users").updateMany( + { id: { $in: users } }, + { + // @ts-expect-error + $pull: { + entities: { id: entity }, }, }, ); export const deleteEntity = async (entity: Entity) => { - await db.collection("entities").deleteOne({id: entity.id}) - await db.collection("roles").deleteMany({entityID: entity.id}) + await db.collection("entities").deleteOne({ id: entity.id }) + await db.collection("roles").deleteMany({ entityID: entity.id }) await db.collection("users").updateMany( - {"entities.id": entity.id}, + { "entities.id": entity.id }, { // @ts-expect-error $pull: { - entities: {id: entity.id}, + entities: { id: entity.id }, }, }, ); diff --git a/src/utils/roles.be.ts b/src/utils/roles.be.ts index 0dea6694..dd3a3b6b 100644 --- a/src/utils/roles.be.ts +++ b/src/utils/roles.be.ts @@ -1,4 +1,4 @@ -import {Role} from "@/interfaces/entity"; +import { Role } from "@/interfaces/entity"; import client from "@/lib/mongodb"; const db = client.db(process.env.MONGODB_DB); @@ -6,16 +6,18 @@ const db = client.db(process.env.MONGODB_DB); export const getRolesByEntities = async (entityIDs: string[]) => await db .collection("roles") - .find({entityID: {$in: entityIDs}}) + .find({ entityID: { $in: entityIDs } }) .toArray(); -export const getRolesByEntity = async (entityID: string) => await db.collection("roles").find({entityID}).toArray(); +export const getRolesByEntity = async (entityID: string) => await db.collection("roles").find({ entityID }).toArray(); -export const getRoles = async (ids?: string[]) => await db.collection("roles").find(!ids ? {} : {id: {$in: ids}}).toArray(); -export const getRole = async (id: string) => (await db.collection("roles").findOne({id})) ?? undefined; +export const getRoles = async (ids?: string[]) => await db.collection("roles").find(!ids ? {} : { id: { $in: ids } }).toArray(); +export const getRole = async (id: string) => (await db.collection("roles").findOne({ id })) ?? undefined; + +export const getDefaultRole = async (entityID: string) => await db.collection("roles").findOne({ isDefault: true, entityID }) export const createRole = async (role: Role) => await db.collection("roles").insertOne(role) -export const deleteRole = async (id: string) => await db.collection("roles").deleteOne({id}) +export const deleteRole = async (id: string) => await db.collection("roles").deleteOne({ id }) export const transferRole = async (previousRole: string, newRole: string) => await db.collection("users")