From ce35ba71f4844ed3655f395cfe76bcaa66ad2fc1 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 11 Dec 2024 18:32:29 +0000 Subject: [PATCH] Improved a bit of the speed of the application --- src/pages/dashboard/admin.tsx | 103 +++++++++++++----------------- src/pages/dashboard/corporate.tsx | 66 ++++++------------- src/pages/dashboard/developer.tsx | 103 +++++++++++++----------------- src/utils/assignments.be.ts | 6 ++ src/utils/groups.be.ts | 6 ++ src/utils/users.be.ts | 27 +++++++- 6 files changed, 148 insertions(+), 163 deletions(-) diff --git a/src/pages/dashboard/admin.tsx b/src/pages/dashboard/admin.tsx index ecfcbfc2..187e0332 100644 --- a/src/pages/dashboard/admin.tsx +++ b/src/pages/dashboard/admin.tsx @@ -2,34 +2,29 @@ import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; -import { Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; import { Assignment } from "@/interfaces/results"; -import { Group, Stat, User } from "@/interfaces/user"; +import { Group, Stat, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be"; +import { countEntitiesAssignments, getAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; -import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; +import { countGroups, getGroups } from "@/utils/groups.be"; import { checkAccess } from "@/utils/permissions"; import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; import { groupByExam } from "@/utils/stats"; import { getStatsByUsers } from "@/utils/stats.be"; -import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { countUsers, getUser, getUsers } from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; import { uniqBy } from "lodash"; -import moment from "moment"; import Head from "next/head"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useMemo } from "react"; import { BsBank, BsClipboard2Data, - BsClock, BsEnvelopePaper, - BsPaperclip, BsPencilSquare, BsPeople, BsPeopleFill, @@ -40,11 +35,14 @@ import { ToastContainer } from "react-toastify"; interface Props { user: User; - users: User[]; + students: User[]; + latestStudents: User[] + latestTeachers: User[] entities: EntityWithRoles[]; - assignments: Assignment[]; + usersCount: { [key in Type]: number } + assignmentsCount: number; stats: Stat[]; - groups: Group[]; + groupsCount: number; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { @@ -53,48 +51,39 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { if (!checkAccess(user, ["admin", "developer"])) return redirect("/") - const users = await getUsers(); - const entities = await getEntitiesWithRoles(); - const assignments = await getAssignments(); - const stats = await getStatsByUsers(users.map((u) => u.id)); - const groups = await getGroups(); + const students = await getUsers({ type: 'student' }); + const usersCount = { + student: await countUsers({ type: "student" }), + teacher: await countUsers({ type: "teacher" }), + corporate: await countUsers({ type: "corporate" }), + mastercorporate: await countUsers({ type: "mastercorporate" }), + } - return { props: serialize({ user, users, entities, assignments, stats, groups }) }; + const latestStudents = await getUsers({ type: 'student' }, 10, { registrationDate: -1 }) + const latestTeachers = await getUsers({ type: 'teacher' }, 10, { registrationDate: -1 }) + + const entities = await getEntitiesWithRoles(); + 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 }) }; }, sessionOptions); -export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]); - const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]); - const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]); - +export default function Dashboard({ + user, + students, + latestStudents, + latestTeachers, + usersCount, + entities, + assignmentsCount, + stats, + groupsCount +}: Props) { const router = useRouter(); - const averageLevelCalculator = (studentStats: Stat[]) => { - const formattedStats = studentStats - .map((s) => ({ - focus: students.find((u) => u.id === s.user)?.focus, - score: s.score, - module: s.module, - })) - .filter((f) => !!f.focus); - const bandScores = formattedStats.map((s) => ({ - module: s.module, - level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), - })); - - const levels: { [key in Module]: number } = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, - level: 0, - }; - bandScores.forEach((b) => (levels[b.module] += b.level)); - - return calculateAverageLevel(levels); - }; - return ( <> @@ -113,35 +102,35 @@ export default function Dashboard({ user, users, entities, assignments, stats, g onClick={() => router.push("/users?type=student")} Icon={BsPersonFill} label="Students" - value={students.length} + value={usersCount.student} color="purple" /> router.push("/users?type=teacher")} Icon={BsPencilSquare} label="Teachers" - value={teachers.length} + value={usersCount.teacher} color="purple" /> router.push("/users?type=corporate")} label="Corporates" - value={corporates.length} + value={usersCount.corporate} color="purple" /> router.push("/users?type=mastercorporate")} label="Master Corporates" - value={masterCorporates.length} + value={usersCount.mastercorporate} color="purple" /> router.push("/classrooms")} label="Classrooms" - value={groups.length} + value={groupsCount} color="purple" /> router.push("/assignments")} label="Assignments" - value={assignments.filter((a) => !a.archived).length} + value={assignmentsCount} color="purple" />
dateSorter(a, b, "desc", "registrationDate"))} + users={latestStudents} title="Latest Students" /> dateSorter(a, b, "desc", "registrationDate"))} + users={latestTeachers} title="Latest Teachers" /> { @@ -57,14 +54,16 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const entities = await getEntitiesWithRoles(entityIDS); const users = await filterAllowedUsers(user, entities) - const assignments = await getEntitiesAssignments(entityIDS); - const stats = await getStatsByUsers(users.map((u) => u.id)); - const groups = await getGroupsByEntities(entityIDS); + const userCounts = await countAllowedUsers(user, entities) + const assignmentsCount = await countEntitiesAssignments(entityIDS, { archived: { $ne: true } }); + const groupsCount = await countGroupsByEntities(entityIDS); - return { props: serialize({ user, users, entities, assignments, stats, groups }) }; + const stats = await getStatsByUsers(users.map((u) => u.id)); + + return { props: serialize({ user, users, userCounts, entities, assignmentsCount, stats, groupsCount }) }; }, sessionOptions); -export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) { +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]); @@ -72,31 +71,6 @@ export default function Dashboard({ user, users, entities, assignments, stats, g const router = useRouter(); - const averageLevelCalculator = (studentStats: Stat[]) => { - const formattedStats = studentStats - .map((s) => ({ - focus: students.find((u) => u.id === s.user)?.focus, - score: s.score, - module: s.module, - })) - .filter((f) => !!f.focus); - const bandScores = formattedStats.map((s) => ({ - module: s.module, - level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), - })); - - const levels: { [key in Module]: number } = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, - level: 0, - }; - bandScores.forEach((b) => (levels[b.module] += b.level)); - - return calculateAverageLevel(levels); - }; - return ( <> @@ -121,21 +95,21 @@ export default function Dashboard({ user, users, entities, assignments, stats, g onClick={() => router.push("/users?type=student")} Icon={BsPersonFill} label="Students" - value={students.length} + value={userCounts.student} color="purple" /> router.push("/users?type=teacher")} Icon={BsPencilSquare} label="Teachers" - value={teachers.length} + value={userCounts.teacher} color="purple" /> router.push("/classrooms")} Icon={BsPeople} label="Classrooms" - value={groups.length} + value={groupsCount} color="purple" /> router.push("/users/performance")} label="Student Performance" - value={students.length} + value={userCounts.student} color="purple" /> router.push("/assignments")} label="Assignments" - value={assignments.filter((a) => !a.archived).length} + value={assignmentsCount} color="purple" />
diff --git a/src/pages/dashboard/developer.tsx b/src/pages/dashboard/developer.tsx index ecfcbfc2..187e0332 100644 --- a/src/pages/dashboard/developer.tsx +++ b/src/pages/dashboard/developer.tsx @@ -2,34 +2,29 @@ import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; -import { Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; import { Assignment } from "@/interfaces/results"; -import { Group, Stat, User } from "@/interfaces/user"; +import { Group, Stat, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be"; +import { countEntitiesAssignments, getAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; -import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; +import { countGroups, getGroups } from "@/utils/groups.be"; import { checkAccess } from "@/utils/permissions"; import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; import { groupByExam } from "@/utils/stats"; import { getStatsByUsers } from "@/utils/stats.be"; -import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { countUsers, getUser, getUsers } from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; import { uniqBy } from "lodash"; -import moment from "moment"; import Head from "next/head"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useMemo } from "react"; import { BsBank, BsClipboard2Data, - BsClock, BsEnvelopePaper, - BsPaperclip, BsPencilSquare, BsPeople, BsPeopleFill, @@ -40,11 +35,14 @@ import { ToastContainer } from "react-toastify"; interface Props { user: User; - users: User[]; + students: User[]; + latestStudents: User[] + latestTeachers: User[] entities: EntityWithRoles[]; - assignments: Assignment[]; + usersCount: { [key in Type]: number } + assignmentsCount: number; stats: Stat[]; - groups: Group[]; + groupsCount: number; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { @@ -53,48 +51,39 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { if (!checkAccess(user, ["admin", "developer"])) return redirect("/") - const users = await getUsers(); - const entities = await getEntitiesWithRoles(); - const assignments = await getAssignments(); - const stats = await getStatsByUsers(users.map((u) => u.id)); - const groups = await getGroups(); + const students = await getUsers({ type: 'student' }); + const usersCount = { + student: await countUsers({ type: "student" }), + teacher: await countUsers({ type: "teacher" }), + corporate: await countUsers({ type: "corporate" }), + mastercorporate: await countUsers({ type: "mastercorporate" }), + } - return { props: serialize({ user, users, entities, assignments, stats, groups }) }; + const latestStudents = await getUsers({ type: 'student' }, 10, { registrationDate: -1 }) + const latestTeachers = await getUsers({ type: 'teacher' }, 10, { registrationDate: -1 }) + + const entities = await getEntitiesWithRoles(); + 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 }) }; }, sessionOptions); -export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const teachers = useMemo(() => users.filter((u) => u.type === "teacher"), [users]); - const corporates = useMemo(() => users.filter((u) => u.type === "corporate"), [users]); - const masterCorporates = useMemo(() => users.filter((u) => u.type === "mastercorporate"), [users]); - +export default function Dashboard({ + user, + students, + latestStudents, + latestTeachers, + usersCount, + entities, + assignmentsCount, + stats, + groupsCount +}: Props) { const router = useRouter(); - const averageLevelCalculator = (studentStats: Stat[]) => { - const formattedStats = studentStats - .map((s) => ({ - focus: students.find((u) => u.id === s.user)?.focus, - score: s.score, - module: s.module, - })) - .filter((f) => !!f.focus); - const bandScores = formattedStats.map((s) => ({ - module: s.module, - level: calculateBandScore(s.score.correct, s.score.total, s.module, s.focus!), - })); - - const levels: { [key in Module]: number } = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, - level: 0, - }; - bandScores.forEach((b) => (levels[b.module] += b.level)); - - return calculateAverageLevel(levels); - }; - return ( <> @@ -113,35 +102,35 @@ export default function Dashboard({ user, users, entities, assignments, stats, g onClick={() => router.push("/users?type=student")} Icon={BsPersonFill} label="Students" - value={students.length} + value={usersCount.student} color="purple" /> router.push("/users?type=teacher")} Icon={BsPencilSquare} label="Teachers" - value={teachers.length} + value={usersCount.teacher} color="purple" /> router.push("/users?type=corporate")} label="Corporates" - value={corporates.length} + value={usersCount.corporate} color="purple" /> router.push("/users?type=mastercorporate")} label="Master Corporates" - value={masterCorporates.length} + value={usersCount.mastercorporate} color="purple" /> router.push("/classrooms")} label="Classrooms" - value={groups.length} + value={groupsCount} color="purple" /> router.push("/assignments")} label="Assignments" - value={assignments.filter((a) => !a.archived).length} + value={assignmentsCount} color="purple" />
dateSorter(a, b, "desc", "registrationDate"))} + users={latestStudents} title="Latest Students" /> dateSorter(a, b, "desc", "registrationDate"))} + users={latestTeachers} title="Latest Teachers" /> { .toArray(); }; +export const countEntitiesAssignments = async (ids: string[], filter?: object) => { + return await db + .collection("assignments") + .countDocuments({ entity: { $in: ids }, ...(filter || {}) }) +}; + export const getAssignmentsForCorporates = async (userType: Type, idsList: string[], startDate?: Date, endDate?: Date) => { const assigners = await Promise.all( idsList.map(async (id) => { diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 70973475..0c0a3536 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -173,3 +173,9 @@ export const getGroupsByEntities = async (ids: string[]): Promise + await db.collection("groups").countDocuments({}) + +export const countGroupsByEntities = async (ids: string[]) => + await db.collection("groups").countDocuments({ entity: { $in: ids } }) diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index 5743f418..3e4e6ac8 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -11,12 +11,19 @@ import { mapBy } from "."; const db = client.db(process.env.MONGODB_DB); -export async function getUsers(filter?: object) { +export async function getUsers(filter?: object, limit = 0, sort = {}) { return await db .collection("users") .find(filter || {}, { projection: { _id: 0 } }) + .limit(limit) + .sort(sort) .toArray(); } +export async function countUsers(filter?: object) { + return await db + .collection("users") + .countDocuments(filter || {}) +} export async function getUserWithEntity(id: string): Promise | undefined> { const user = await db.collection("users").findOne({ id: id }, { projection: { _id: 0 } }); @@ -68,8 +75,8 @@ export async function getEntitiesUsers(ids: string[], filter?: object, limit?: n .toArray(); } -export async function countEntitiesUsers(ids: string[]) { - return await db.collection("users").countDocuments({ "entities.id": { $in: ids } }); +export async function countEntitiesUsers(ids: string[], filter?: object) { + return await db.collection("users").countDocuments({ "entities.id": { $in: ids }, ...(filter || {}) }); } export async function getLinkedUsers( @@ -154,3 +161,17 @@ export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[] return [...students, ...teachers, ...corporates, ...masterCorporates] } + +export const countAllowedUsers = async (user: User, entities: EntityWithRoles[]) => { + const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students') + const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers') + const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates') + const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates') + + const students = await countEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" }) + const teachers = await countEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), { type: "teacher" }) + const corporates = await countEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), { type: "corporate" }) + const masterCorporates = await countEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), { type: "mastercorporate" }) + + return { students, teachers, corporates, masterCorporates } +}