From 564e6438cbd7e3b914f331c149b0192b47e82e2f Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 1 Oct 2024 17:39:43 +0100 Subject: [PATCH] Continued creating the entity system --- src/components/High/CardList.tsx | 32 ++ src/components/Medium/InviteWithUserCard.tsx | 67 ++++ src/dashboards/Corporate/index.tsx | 42 +-- src/dashboards/IconCard.tsx | 6 +- src/interfaces/entity.ts | 11 +- src/interfaces/index.ts | 1 + src/interfaces/invite.ts | 12 +- src/interfaces/results.ts | 1 + src/interfaces/user.ts | 3 +- src/pages/api/entities/[id]/index.ts | 46 +++ src/pages/api/entities/[id]/users.ts | 65 ++++ src/pages/api/entities/index.ts | 48 +++ src/pages/api/hello.ts | 18 +- src/pages/dashboard/admin.tsx | 192 +++++++++++ src/pages/dashboard/corporate.tsx | 208 +++++++++++ src/pages/dashboard/developer.tsx | 192 +++++++++++ src/pages/dashboard/index.tsx | 27 ++ src/pages/dashboard/mastercorporate.tsx | 198 +++++++++++ src/pages/dashboard/student.tsx | 298 ++++++++++++++++ src/pages/dashboard/teacher.tsx | 182 ++++++++++ src/pages/entities/[id]/index.tsx | 343 +++++++++++++++++++ src/pages/entities/[id]/settings.tsx | 232 +++++++++++++ src/pages/entities/index.tsx | 116 +++++++ src/pages/groups/[id].tsx | 3 +- src/pages/groups/index.tsx | 102 +++--- src/pages/index.tsx | 12 +- src/utils/assignments.be.ts | 19 + src/utils/entities.be.ts | 35 ++ src/utils/exams.be.ts | 19 +- src/utils/grading.be.ts | 3 + src/utils/groups.be.ts | 8 + src/utils/index.ts | 8 + src/utils/invites.be.ts | 18 + src/utils/roles.be.ts | 14 + src/utils/sessions.be.ts | 11 + src/utils/stats.be.ts | 12 + src/utils/users.be.ts | 48 ++- 37 files changed, 2522 insertions(+), 130 deletions(-) create mode 100644 src/components/High/CardList.tsx create mode 100644 src/components/Medium/InviteWithUserCard.tsx create mode 100644 src/pages/api/entities/[id]/index.ts create mode 100644 src/pages/api/entities/[id]/users.ts create mode 100644 src/pages/api/entities/index.ts create mode 100644 src/pages/dashboard/admin.tsx create mode 100644 src/pages/dashboard/corporate.tsx create mode 100644 src/pages/dashboard/developer.tsx create mode 100644 src/pages/dashboard/index.tsx create mode 100644 src/pages/dashboard/mastercorporate.tsx create mode 100644 src/pages/dashboard/student.tsx create mode 100644 src/pages/dashboard/teacher.tsx create mode 100644 src/pages/entities/[id]/index.tsx create mode 100644 src/pages/entities/[id]/settings.tsx create mode 100644 src/pages/entities/index.tsx create mode 100644 src/utils/entities.be.ts create mode 100644 src/utils/invites.be.ts create mode 100644 src/utils/roles.be.ts create mode 100644 src/utils/sessions.be.ts create mode 100644 src/utils/stats.be.ts diff --git a/src/components/High/CardList.tsx b/src/components/High/CardList.tsx new file mode 100644 index 00000000..d54e7107 --- /dev/null +++ b/src/components/High/CardList.tsx @@ -0,0 +1,32 @@ +import {useListSearch} from "@/hooks/useListSearch"; +import usePagination from "@/hooks/usePagination"; +import {ReactNode} from "react"; +import Checkbox from "../Low/Checkbox"; +import Separator from "../Low/Separator"; + +interface Props { + list: T[]; + searchFields: string[][]; + pageSize?: number; + firstCard?: () => ReactNode; + renderCard: (item: T) => ReactNode; +} + +export default function CardList({list, searchFields, renderCard, firstCard, pageSize = 20}: Props) { + const {rows, renderSearch} = useListSearch(searchFields, list); + + const {items, page, renderMinimal} = usePagination(rows, pageSize); + + return ( +
+
+ {renderSearch()} + {renderMinimal()} +
+
+ {page === 0 && !!firstCard && firstCard()} + {items.map(renderCard)} +
+
+ ); +} diff --git a/src/components/Medium/InviteWithUserCard.tsx b/src/components/Medium/InviteWithUserCard.tsx new file mode 100644 index 00000000..158723f7 --- /dev/null +++ b/src/components/Medium/InviteWithUserCard.tsx @@ -0,0 +1,67 @@ +import {Invite, InviteWithUsers} from "@/interfaces/invite"; +import {User} from "@/interfaces/user"; +import {getUserName} from "@/utils/users"; +import axios from "axios"; +import {useMemo, useState} from "react"; +import {BsArrowRepeat} from "react-icons/bs"; +import {toast} from "react-toastify"; + +interface Props { + invite: InviteWithUsers; + reload: () => void; +} + +export default function InviteWithUserCard({invite, reload}: Props) { + const [isLoading, setIsLoading] = useState(false); + + const name = useMemo(() => (!invite.from ? null : getUserName(invite.from)), [invite.from]); + + const decide = (decision: "accept" | "decline") => { + if (!confirm(`Are you sure you want to ${decision} this invite?`)) return; + + setIsLoading(true); + axios + .get(`/api/invites/${decision}/${invite.id}`) + .then(() => { + toast.success(`Successfully ${decision === "accept" ? "accepted" : "declined"} the invite!`, {toastId: "success"}); + reload(); + }) + .catch((e) => { + toast.success(`Something went wrong, please try again later!`, { + toastId: "error", + }); + reload(); + }) + .finally(() => setIsLoading(false)); + }; + + return ( +
+ Invited by {name} +
+ + +
+
+ ); +} diff --git a/src/dashboards/Corporate/index.tsx b/src/dashboards/Corporate/index.tsx index e16028b3..453a9005 100644 --- a/src/dashboards/Corporate/index.tsx +++ b/src/dashboards/Corporate/index.tsx @@ -1,61 +1,40 @@ /* eslint-disable @next/next/no-img-element */ import Modal from "@/components/Modal"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import useUsers, {userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers"; +import useUsers from "@/hooks/useUsers"; import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; -import {dateSorter} from "@/utils"; +import {dateSorter, mapBy} from "@/utils"; import moment from "moment"; import {useEffect, useMemo, useState} from "react"; import { BsArrowLeft, BsClipboard2Data, - BsClipboard2DataFill, BsClock, - BsGlobeCentralSouthAsia, BsPaperclip, - BsPerson, - BsPersonAdd, BsPersonFill, BsPersonFillGear, - BsPersonGear, BsPencilSquare, - BsPersonBadge, BsPersonCheck, BsPeople, - BsArrowRepeat, - BsPlus, BsEnvelopePaper, BsDatabase, } from "react-icons/bs"; import UserCard from "@/components/UserCard"; import useGroups from "@/hooks/useGroups"; -import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; +import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; import {Module} from "@/interfaces"; import {groupByExam} from "@/utils/stats"; import IconCard from "../IconCard"; import GroupList from "@/pages/(admin)/Lists/GroupList"; import useFilterStore from "@/stores/listFilterStore"; import {useRouter} from "next/router"; -import useCodes from "@/hooks/useCodes"; -import {getUserCorporate} from "@/utils/groups"; import useAssignments from "@/hooks/useAssignments"; -import {Assignment} from "@/interfaces/results"; -import AssignmentView from "../AssignmentView"; -import AssignmentCreator from "../AssignmentCreator"; -import clsx from "clsx"; -import AssignmentCard from "../AssignmentCard"; -import {createColumnHelper} from "@tanstack/react-table"; -import Checkbox from "@/components/Low/Checkbox"; -import List from "@/components/List"; -import {getUserCompanyName} from "@/resources/user"; -import {futureAssignmentFilter, pastAssignmentFilter, archivedAssignmentFilter, activeAssignmentFilter} from "@/utils/assignments"; import useUserBalance from "@/hooks/useUserBalance"; import AssignmentsPage from "../views/AssignmentsPage"; import StudentPerformancePage from "./StudentPerformancePage"; -import MasterStatistical from "../MasterCorporate/MasterStatistical"; import MasterStatisticalPage from "./MasterStatisticalPage"; +import {getEntitiesUsers} from "@/utils/users.be"; interface Props { user: CorporateUser; @@ -91,19 +70,6 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) { const assignmentsGroups = useMemo(() => groups.filter((x) => x.admin === user.id || x.participants.includes(user.id)), [groups, user.id]); - const assignmentsUsers = useMemo( - () => - [...teachers, ...students].filter((x) => - !!selectedUser - ? groups - .filter((g) => g.admin === selectedUser.id) - .flatMap((g) => g.participants) - .includes(x.id) || false - : groups.flatMap((g) => g.participants).includes(x.id), - ), - [groups, teachers, students, selectedUser], - ); - useEffect(() => { setShowModal(!!selectedUser && router.asPath === "/#"); }, [selectedUser, router.asPath]); diff --git a/src/dashboards/IconCard.tsx b/src/dashboards/IconCard.tsx index e21e1e05..dcea0834 100644 --- a/src/dashboards/IconCard.tsx +++ b/src/dashboards/IconCard.tsx @@ -22,10 +22,10 @@ export default function IconCard({Icon, label, value, color, tooltip, onClick, c }; return ( -
-
+ ); } diff --git a/src/interfaces/entity.ts b/src/interfaces/entity.ts index def83ddb..071e031a 100644 --- a/src/interfaces/entity.ts +++ b/src/interfaces/entity.ts @@ -3,14 +3,17 @@ export interface Entity { label: string; } -export interface Roles { +export interface Role { id: string; + entityID: string; permissions: string[]; label: string; } -export interface EntityWithPermissions extends Entity { - roles: Roles[]; +export interface EntityWithRoles extends Entity { + roles: Role[]; } -export type WithEntity = T extends {entities: string[]} ? T & {entities: Entity[]} : T; +export type WithEntity = T extends {entities: {id: string; role: string}[]} + ? Omit & {entities: {entity?: Entity; role?: Role}[]} + : T; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index cd740bd4..5bbe259a 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -8,5 +8,6 @@ export interface Step { export interface Grading { user: string; + entity?: string; steps: Step[]; } diff --git a/src/interfaces/invite.ts b/src/interfaces/invite.ts index ce5e9f5e..4dda38b7 100644 --- a/src/interfaces/invite.ts +++ b/src/interfaces/invite.ts @@ -1,5 +1,11 @@ +import {User} from "./user"; + export interface Invite { - id: string; - from: string; - to: string; + id: string; + from: string; + to: string; +} + +export interface InviteWithUsers extends Omit { + from?: User; } diff --git a/src/interfaces/results.ts b/src/interfaces/results.ts index ebc26bd8..cd9ff373 100644 --- a/src/interfaces/results.ts +++ b/src/interfaces/results.ts @@ -34,6 +34,7 @@ export interface Assignment { start?: boolean; autoStartDate?: Date; autoStart?: boolean; + entity?: string; } export type AssignmentWithCorporateId = Assignment & {corporateId: string}; diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 4609a17e..54fba92a 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -22,7 +22,7 @@ export interface BasicUser { status: UserStatus; permissions: PermissionType[]; lastLogin?: Date; - entities: string[]; + entities: {id: string; role: string}[]; } export interface StudentUser extends BasicUser { @@ -150,6 +150,7 @@ export interface Group { participants: string[]; id: string; disableEditing?: boolean; + entity?: string; } export interface GroupWithUsers extends Omit { diff --git a/src/pages/api/entities/[id]/index.ts b/src/pages/api/entities/[id]/index.ts new file mode 100644 index 00000000..df9762b9 --- /dev/null +++ b/src/pages/api/entities/[id]/index.ts @@ -0,0 +1,46 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type {NextApiRequest, NextApiResponse} from "next"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {getEntity, getEntityWithRoles} from "@/utils/entities.be"; +import client from "@/lib/mongodb"; +import {Entity} from "@/interfaces/entity"; + +const db = client.db(process.env.MONGODB_DB); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "get") return await get(req, res); + if (req.method === "PATCH") return await patch(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const {id, showRoles} = req.query as {id: string; showRoles: string}; + + const entity = await (!!showRoles ? getEntityWithRoles : getEntity)(id); + res.status(200).json(entity); +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const {id} = req.query as {id: string}; + + const user = req.session.user; + if (!user.entities.map((x) => x.id).includes(id)) { + return res.status(403).json({ok: false}); + } + + const entity = await db.collection("entities").updateOne({id}, {$set: {label: req.body.label}}); + + return res.status(200).json({ok: entity.acknowledged}); +} diff --git a/src/pages/api/entities/[id]/users.ts b/src/pages/api/entities/[id]/users.ts new file mode 100644 index 00000000..044f9c26 --- /dev/null +++ b/src/pages/api/entities/[id]/users.ts @@ -0,0 +1,65 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type {NextApiRequest, NextApiResponse} from "next"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {countEntityUsers, getEntityUsers} from "@/utils/users.be"; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "get") return await get(req, res); + if (req.method === "PATCH") return await patch(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const {id, onlyCount} = req.query as {id: string; onlyCount: string}; + + if (onlyCount) return res.status(200).json(await countEntityUsers(id)); + + const users = await getEntityUsers(id); + res.status(200).json(users); +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const {id} = req.query as {id: string}; + const {add, members, role} = req.body as {add: boolean; members: string[]; role?: string}; + + if (add) { + await db.collection("users").updateMany( + {id: {$in: members}}, + { + // @ts-expect-error + $push: { + entities: {id, role}, + }, + }, + ); + + return res.status(204).end(); + } + + await db.collection("users").updateMany( + {id: {$in: members}}, + { + // @ts-expect-error + $pull: { + entities: {id}, + }, + }, + ); + + return res.status(204).end(); +} diff --git a/src/pages/api/entities/index.ts b/src/pages/api/entities/index.ts new file mode 100644 index 00000000..a29cf88a --- /dev/null +++ b/src/pages/api/entities/index.ts @@ -0,0 +1,48 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type {NextApiRequest, NextApiResponse} from "next"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; +import {Entity} from "@/interfaces/entity"; +import {v4} from "uuid"; + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return await get(req, res); + if (req.method === "POST") return await post(req, res); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const user = req.session.user; + const {showRoles} = req.query as {showRoles: string}; + + const getFn = showRoles ? getEntitiesWithRoles : getEntities; + + if (["admin", "developer"].includes(user.type)) return res.status(200).json(await getFn()); + res.status(200).json(await getFn(user.entities.map((x) => x.id))); +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const user = req.session.user; + if (!["admin", "developer"].includes(user.type)) { + return res.status(403).json({ok: false}); + } + + const entity: Entity = { + id: v4(), + label: req.body.label, + }; + + return res.status(200).json(entity); +} diff --git a/src/pages/api/hello.ts b/src/pages/api/hello.ts index f8bcc7e5..d301b5bf 100644 --- a/src/pages/api/hello.ts +++ b/src/pages/api/hello.ts @@ -1,13 +1,15 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from 'next' +import client from "@/lib/mongodb"; +import type {NextApiRequest, NextApiResponse} from "next"; + +const db = client.db(process.env.MONGODB_DB); type Data = { - name: string -} + name: string; +}; -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - res.status(200).json({ name: 'John Doe' }) +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + await db.collection("users").updateMany({}, {$set: {entities: []}}); + + res.status(200).json({name: "John Doe"}); } diff --git a/src/pages/dashboard/admin.tsx b/src/pages/dashboard/admin.tsx new file mode 100644 index 00000000..dfe4ed7c --- /dev/null +++ b/src/pages/dashboard/admin.tsx @@ -0,0 +1,192 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import IconCard from "@/dashboards/IconCard"; +import {Module} from "@/interfaces"; +import {EntityWithRoles} from "@/interfaces/entity"; +import {Assignment} from "@/interfaces/results"; +import {Group, Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {dateSorter, filterBy, mapBy, serialize} from "@/utils"; +import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be"; +import {getEntitiesWithRoles} from "@/utils/entities.be"; +import {getGroups, getGroupsByEntities} 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 {withIronSessionSsr} from "iron-session/next"; +import {uniqBy} from "lodash"; +import moment from "moment"; +import Head from "next/head"; +import Link from "next/link"; +import {useMemo} from "react"; +import { + BsBank, + BsClipboard2Data, + BsClock, + BsEnvelopePaper, + BsPaperclip, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, +} from "react-icons/bs"; +import {ToastContainer} from "react-toastify"; + +interface Props { + user: User; + users: User[]; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + groups: Group[]; +} + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (!checkAccess(user, ["admin", "developer"])) + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + + 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(); + + return {props: serialize({user, users, entities, assignments, stats, groups})}; +}, 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]); + + 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); + }; + + const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + return ( + <> + + EnCoach + + + + + + +
+ + + + + + + + + + !a.archived).length} color="purple" /> +
+ +
+
+ Latest students +
+ {students + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {teachers + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {students + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {students + .sort( + (a, b) => + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + .map((x) => ( + + ))} +
+
+
+
+ + ); +} diff --git a/src/pages/dashboard/corporate.tsx b/src/pages/dashboard/corporate.tsx new file mode 100644 index 00000000..5008a146 --- /dev/null +++ b/src/pages/dashboard/corporate.tsx @@ -0,0 +1,208 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import IconCard from "@/dashboards/IconCard"; +import {Module} from "@/interfaces"; +import {EntityWithRoles} from "@/interfaces/entity"; +import {Assignment} from "@/interfaces/results"; +import {Group, Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {dateSorter, filterBy, mapBy, serialize} from "@/utils"; +import {getEntitiesAssignments} from "@/utils/assignments.be"; +import {getEntitiesWithRoles} from "@/utils/entities.be"; +import {getGroupsByEntities} 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} 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 {useMemo} from "react"; +import { + BsClipboard2Data, + BsClock, + BsEnvelopePaper, + BsPaperclip, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, +} from "react-icons/bs"; +import {ToastContainer} from "react-toastify"; + +interface Props { + user: User; + users: User[]; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + groups: Group[]; +} + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (!checkAccess(user, ["admin", "developer", "corporate"])) + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + + const entityIDS = mapBy(user.entities, "id") || []; + + const users = await getEntitiesUsers(entityIDS); + const entities = await getEntitiesWithRoles(entityIDS); + const assignments = await getEntitiesAssignments(entityIDS); + const stats = await getStatsByUsers(users.map((u) => u.id)); + const groups = await getGroupsByEntities(entityIDS); + + return {props: serialize({user, users, entities, assignments, stats, groups})}; +}, 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 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); + }; + + const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + return ( + <> + + EnCoach + + + + + + +
+ {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} +
+ + + + + + + + + !a.archived).length} + color="purple" + /> +
+
+ +
+
+ Latest students +
+ {students + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {teachers + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {students + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {students + .sort( + (a, b) => + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + .map((x) => ( + + ))} +
+
+
+
+ + ); +} diff --git a/src/pages/dashboard/developer.tsx b/src/pages/dashboard/developer.tsx new file mode 100644 index 00000000..dfe4ed7c --- /dev/null +++ b/src/pages/dashboard/developer.tsx @@ -0,0 +1,192 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import IconCard from "@/dashboards/IconCard"; +import {Module} from "@/interfaces"; +import {EntityWithRoles} from "@/interfaces/entity"; +import {Assignment} from "@/interfaces/results"; +import {Group, Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {dateSorter, filterBy, mapBy, serialize} from "@/utils"; +import {getAssignments, getEntitiesAssignments} from "@/utils/assignments.be"; +import {getEntitiesWithRoles} from "@/utils/entities.be"; +import {getGroups, getGroupsByEntities} 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 {withIronSessionSsr} from "iron-session/next"; +import {uniqBy} from "lodash"; +import moment from "moment"; +import Head from "next/head"; +import Link from "next/link"; +import {useMemo} from "react"; +import { + BsBank, + BsClipboard2Data, + BsClock, + BsEnvelopePaper, + BsPaperclip, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, +} from "react-icons/bs"; +import {ToastContainer} from "react-toastify"; + +interface Props { + user: User; + users: User[]; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + groups: Group[]; +} + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (!checkAccess(user, ["admin", "developer"])) + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + + 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(); + + return {props: serialize({user, users, entities, assignments, stats, groups})}; +}, 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]); + + 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); + }; + + const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + return ( + <> + + EnCoach + + + + + + +
+ + + + + + + + + + !a.archived).length} color="purple" /> +
+ +
+
+ Latest students +
+ {students + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {teachers + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {students + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {students + .sort( + (a, b) => + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + .map((x) => ( + + ))} +
+
+
+
+ + ); +} diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx new file mode 100644 index 00000000..41caa641 --- /dev/null +++ b/src/pages/dashboard/index.tsx @@ -0,0 +1,27 @@ +import {User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {withIronSessionSsr} from "iron-session/next"; + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + return { + redirect: { + destination: `/dashboard/${user.type}`, + permanent: false, + }, + }; +}, sessionOptions); + +export default function Dashboard() { + return
; +} diff --git a/src/pages/dashboard/mastercorporate.tsx b/src/pages/dashboard/mastercorporate.tsx new file mode 100644 index 00000000..89ec1243 --- /dev/null +++ b/src/pages/dashboard/mastercorporate.tsx @@ -0,0 +1,198 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import IconCard from "@/dashboards/IconCard"; +import {Module} from "@/interfaces"; +import {EntityWithRoles} from "@/interfaces/entity"; +import {Assignment} from "@/interfaces/results"; +import {Group, Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {dateSorter, filterBy, mapBy, serialize} from "@/utils"; +import {getEntitiesAssignments} from "@/utils/assignments.be"; +import {getEntitiesWithRoles} from "@/utils/entities.be"; +import {getGroupsByEntities} 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} 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 {useMemo} from "react"; +import { + BsBank, + BsClipboard2Data, + BsClock, + BsEnvelopePaper, + BsPaperclip, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, +} from "react-icons/bs"; +import {ToastContainer} from "react-toastify"; + +interface Props { + user: User; + users: User[]; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + groups: Group[]; +} + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + + const entityIDS = mapBy(user.entities, "id") || []; + + const users = await getEntitiesUsers(entityIDS); + const entities = await getEntitiesWithRoles(entityIDS); + const assignments = await getEntitiesAssignments(entityIDS); + const stats = await getStatsByUsers(users.map((u) => u.id)); + const groups = await getGroupsByEntities(entityIDS); + + return {props: serialize({user, users, entities, assignments, stats, groups})}; +}, 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 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); + }; + + const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + return ( + <> + + EnCoach + + + + + + +
+ + + + + + + + + !a.archived).length} color="purple" /> + +
+ +
+
+ Latest students +
+ {students + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Latest teachers +
+ {teachers + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {students + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {students + .sort( + (a, b) => + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + .map((x) => ( + + ))} +
+
+
+
+ + ); +} diff --git a/src/pages/dashboard/student.tsx b/src/pages/dashboard/student.tsx new file mode 100644 index 00000000..adeb1c71 --- /dev/null +++ b/src/pages/dashboard/student.tsx @@ -0,0 +1,298 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import Button from "@/components/Low/Button"; +import ProgressBar from "@/components/Low/ProgressBar"; +import InviteWithUserCard from "@/components/Medium/InviteWithUserCard"; +import ModuleBadge from "@/components/ModuleBadge"; +import ProfileSummary from "@/components/ProfileSummary"; +import {Session} from "@/hooks/useSessions"; +import {Grading} from "@/interfaces"; +import {EntityWithRoles} from "@/interfaces/entity"; +import {Exam} from "@/interfaces/exam"; +import {InviteWithUsers} from "@/interfaces/invite"; +import {Assignment} from "@/interfaces/results"; +import {Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import useExamStore from "@/stores/examStore"; +import {mapBy, serialize} from "@/utils"; +import {activeAssignmentFilter} from "@/utils/assignments"; +import {getAssignmentsByAssignee} from "@/utils/assignments.be"; +import {getEntityWithRoles} from "@/utils/entities.be"; +import {getExamsByIds} from "@/utils/exams.be"; +import {getGradingSystemByEntity} from "@/utils/grading.be"; +import {convertInvitersToUsers, getInvitesByInvitee} from "@/utils/invites.be"; +import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils"; +import {checkAccess} from "@/utils/permissions"; +import {getGradingLabel} from "@/utils/score"; +import {getSessionsByUser} from "@/utils/sessions.be"; +import {averageScore} from "@/utils/stats"; +import {getStatsByUser} from "@/utils/stats.be"; +import clsx from "clsx"; +import {withIronSessionSsr} from "iron-session/next"; +import {capitalize, uniqBy} from "lodash"; +import moment from "moment"; +import Head from "next/head"; +import {useRouter} from "next/router"; +import {BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs"; +import {ToastContainer} from "react-toastify"; + +interface Props { + user: User; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + exams: Exam[]; + sessions: Session[]; + invites: InviteWithUsers[]; + grading: Grading; +} + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + + const entityIDS = mapBy(user.entities, "id") || []; + const entityID = entityIDS.length > 0 ? entityIDS[0] : ""; + + const entities = await getEntityWithRoles(entityID); + const allAssignments = await getAssignmentsByAssignee(user.id, {archived: false}); + const stats = await getStatsByUser(user.id); + const sessions = await getSessionsByUser(user.id, 10); + const invites = await getInvitesByInvitee(user.id); + const grading = await getGradingSystemByEntity(entityID); + + const formattedInvites = await Promise.all(invites.map(convertInvitersToUsers)); + const assignments = allAssignments.filter(activeAssignmentFilter); + + const examIDs = uniqBy( + assignments.flatMap((a) => + a.exams.filter((e) => e.assignee === user.id).map((e) => ({module: e.module, id: e.id, key: `${e.module}_${e.id}`})), + ), + "key", + ); + const exams = await getExamsByIds(examIDs); + + return {props: serialize({user, entities, assignments, stats, exams, sessions, invites: formattedInvites, grading})}; +}, sessionOptions); + +export default function Dashboard({user, entities, assignments, stats, invites, grading, sessions, exams}: Props) { + const router = useRouter(); + + const setExams = useExamStore((state) => state.setExams); + const setShowSolutions = useExamStore((state) => state.setShowSolutions); + const setUserSolutions = useExamStore((state) => state.setUserSolutions); + const setSelectedModules = useExamStore((state) => state.setSelectedModules); + const setAssignment = useExamStore((state) => state.setAssignment); + + const startAssignment = (assignment: Assignment) => { + if (exams.every((x) => !!x)) { + setUserSolutions([]); + setShowSolutions(false); + setExams(exams.map((x) => x!).sort(sortByModule)); + setSelectedModules( + exams + .map((x) => x!) + .sort(sortByModule) + .map((x) => x!.module), + ); + setAssignment(assignment); + + router.push("/exercises"); + } + }; + + const studentAssignments = assignments.filter(activeAssignmentFilter); + + return ( + <> + + EnCoach + + + + + + + {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} + + , + value: countFullExams(stats), + label: "Exams", + tooltip: "Number of all conducted completed exams", + }, + { + icon: , + value: countExamModules(stats), + label: "Modules", + tooltip: "Number of all exam modules performed including Level Test", + }, + { + icon: , + value: `${stats.length > 0 ? averageScore(stats) : 0}%`, + label: "Average Score", + tooltip: "Average success rate for questions responded", + }, + ]} + /> + + {/* Bio */} +
+ Bio + + {user.bio || "Your bio will appear here, you can change it by clicking on your name in the top right corner."} + +
+ + {/* Assignments */} +
+ + {studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."} + {studentAssignments + .sort((a, b) => moment(a.startDate).diff(b.startDate)) + .map((assignment) => ( +
r.user).includes(user.id) && "border-mti-green-light", + )} + key={assignment.id}> +
+

{assignment.name}

+ + {moment(assignment.startDate).format("DD/MM/YY, HH:mm")} + - + {moment(assignment.endDate).format("DD/MM/YY, HH:mm")} + +
+
+
+ {assignment.exams + .filter((e) => e.assignee === user.id) + .map((e) => e.module) + .sort(sortByModuleName) + .map((module) => ( + + ))} +
+ {!assignment.results.map((r) => r.user).includes(user.id) && ( + <> +
+ +
+
x.assignment?.id === assignment.id).length > 0 && "tooltip", + )}> + +
+ + )} + {assignment.results.map((r) => r.user).includes(user.id) && ( + + )} +
+
+ ))} +
+
+ + {/* Invites */} + {invites.length > 0 && ( +
+ + {invites.map((invite) => ( + router.replace(router.asPath)} /> + ))} + +
+ )} + + {/* Score History */} +
+ Score History +
+ {MODULE_ARRAY.map((module) => { + const desiredLevel = user.desiredLevels[module] || 9; + const level = user.levels[module] || 0; + return ( +
+
+
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } +
+
+ {capitalize(module)} + + {module === "level" && !!grading && `English Level: ${getGradingLabel(level, grading.steps)}`} + {module !== "level" && `Level ${level} / Level 9 (Desired Level: ${desiredLevel})`} + +
+
+
+ +
+
+ ); + })} +
+
+
+ + ); +} diff --git a/src/pages/dashboard/teacher.tsx b/src/pages/dashboard/teacher.tsx new file mode 100644 index 00000000..2b9d9080 --- /dev/null +++ b/src/pages/dashboard/teacher.tsx @@ -0,0 +1,182 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import IconCard from "@/dashboards/IconCard"; +import {Module} from "@/interfaces"; +import {EntityWithRoles} from "@/interfaces/entity"; +import {Assignment} from "@/interfaces/results"; +import {Group, Stat, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {dateSorter, filterBy, mapBy, serialize} from "@/utils"; +import {getEntitiesAssignments} from "@/utils/assignments.be"; +import {getEntitiesWithRoles} from "@/utils/entities.be"; +import {getGroupsByEntities} 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} 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 {useMemo} from "react"; +import { + BsClipboard2Data, + BsClock, + BsEnvelopePaper, + BsPaperclip, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, +} from "react-icons/bs"; +import {ToastContainer} from "react-toastify"; + +interface Props { + user: User; + users: User[]; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + groups: Group[]; +} + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User | undefined; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (!checkAccess(user, ["admin", "developer", "teacher"])) + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + + const entityIDS = mapBy(user.entities, "id") || []; + + const users = await getEntitiesUsers(entityIDS); + const entities = await getEntitiesWithRoles(entityIDS); + const assignments = await getEntitiesAssignments(entityIDS); + const stats = await getStatsByUsers(users.map((u) => u.id)); + const groups = await getGroupsByEntities(entityIDS); + + return {props: serialize({user, users, entities, assignments, stats, groups})}; +}, sessionOptions); + +export default function Dashboard({user, users, entities, assignments, stats, groups}: Props) { + const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); + + 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); + }; + + const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + return ( + <> + + EnCoach + + + + + + +
+ {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} +
+ + + + + !a.archived).length} color="purple" /> +
+
+ +
+
+ Latest students +
+ {students + .sort((a, b) => dateSorter(a, b, "desc", "registrationDate")) + .map((x) => ( + + ))} +
+
+
+ Highest level students +
+ {students + .sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) + .map((x) => ( + + ))} +
+
+
+ Highest exam count students +
+ {students + .sort( + (a, b) => + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + .map((x) => ( + + ))} +
+
+
+
+ + ); +} diff --git a/src/pages/entities/[id]/index.tsx b/src/pages/entities/[id]/index.tsx new file mode 100644 index 00000000..85305c29 --- /dev/null +++ b/src/pages/entities/[id]/index.tsx @@ -0,0 +1,343 @@ +/* eslint-disable @next/next/no-img-element */ +import CardList from "@/components/High/CardList"; +import Layout from "@/components/High/Layout"; +import Tooltip from "@/components/Low/Tooltip"; +import {useListSearch} from "@/hooks/useListSearch"; +import usePagination from "@/hooks/usePagination"; +import {Entity, EntityWithRoles, Role} from "@/interfaces/entity"; +import {GroupWithUsers, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {USER_TYPE_LABELS} from "@/resources/user"; +import {getEntityWithRoles} from "@/utils/entities.be"; +import {convertToUsers, getGroup} from "@/utils/groups.be"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import {getUserName} from "@/utils/users"; +import {getEntityUsers, getLinkedUsers, getSpecificUsers} from "@/utils/users.be"; +import axios from "axios"; +import clsx from "clsx"; +import {withIronSessionSsr} from "iron-session/next"; +import moment from "moment"; +import Head from "next/head"; +import Link from "next/link"; +import {useRouter} from "next/router"; +import {Divider} from "primereact/divider"; +import {useEffect, useMemo, useState} from "react"; +import { + BsChevronLeft, + BsClockFill, + BsEnvelopeFill, + BsFillPersonVcardFill, + BsPlus, + BsSquare, + BsStopwatchFill, + BsTag, + BsTrash, + BsX, +} from "react-icons/bs"; +import {toast, ToastContainer} from "react-toastify"; + +export const getServerSideProps = withIronSessionSsr(async ({req, params}) => { + const user = req.session.user as User; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const {id} = params as {id: string}; + + const entityWithRoles = await getEntityWithRoles(id); + if (!entityWithRoles || (checkAccess(user, getTypesOfUser(["admin", "developer"])) && !user.entities.map((x) => x.id).includes(id))) { + return { + redirect: { + destination: "/entities", + permanent: false, + }, + }; + } + + const {entity, roles} = entityWithRoles; + + const linkedUsers = await getLinkedUsers(user.id, user.type); + const entityUsers = await getEntityUsers(id); + + const usersWithRole = entityUsers.map((u) => { + const e = u.entities.find((e) => e.id === id); + return {...u, role: roles.find((r) => r.id === e?.role)}; + }); + + return { + props: { + user, + entity: JSON.parse(JSON.stringify(entity)), + roles: JSON.parse(JSON.stringify(roles)), + users: JSON.parse(JSON.stringify(usersWithRole)), + linkedUsers: JSON.parse(JSON.stringify(linkedUsers.users)), + }, + }; +}, sessionOptions); + +type UserWithRole = User & {role?: Role}; + +interface Props { + user: User; + entity: Entity; + roles: Role[]; + users: UserWithRole[]; + linkedUsers: User[]; +} + +export default function Home({user, entity, roles, users, linkedUsers}: Props) { + const [isAdding, setIsAdding] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + + const router = useRouter(); + + const allowEntityEdit = useMemo(() => checkAccess(user, ["admin", "developer"]), [user]); + + const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id])); + + const removeParticipants = () => { + if (selectedUsers.length === 0) return; + if (!allowEntityEdit) return; + if (!confirm(`Are you sure you want to remove ${selectedUsers.length} member${selectedUsers.length === 1 ? "" : "s"} from this entity?`)) + return; + + setIsLoading(true); + + axios + .patch(`/api/entities/${entity.id}/users`, {add: false, members: selectedUsers}) + .then(() => { + toast.success("The entity has been updated successfully!"); + router.replace(router.asPath); + setSelectedUsers([]); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const addParticipants = () => { + if (selectedUsers.length === 0) return; + if (!allowEntityEdit || !isAdding) return; + if (!confirm(`Are you sure you want to add ${selectedUsers.length} member${selectedUsers.length === 1 ? "" : "s"} to this entity?`)) return; + + setIsLoading(true); + + axios + .patch(`/api/entities/${entity.id}/users`, {add: true, members: selectedUsers, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930"}) + .then(() => { + toast.success("The entity has been updated successfully!"); + router.replace(router.asPath); + setIsAdding(false); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const renameGroup = () => { + if (!allowEntityEdit) return; + + const name = prompt("Rename this entity:", entity.label); + if (!name) return; + + setIsLoading(true); + axios + .patch(`/api/entities/${entity.id}`, {name}) + .then(() => { + toast.success("The entity has been updated successfully!"); + router.replace(router.asPath); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const deleteGroup = () => { + if (!allowEntityEdit) return; + if (!confirm("Are you sure you want to delete this entity?")) return; + + setIsLoading(true); + + axios + .delete(`/api/entities/${entity.id}`) + .then(() => { + toast.success("This entity has been successfully deleted!"); + router.replace("/entities"); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const renderCard = (u: UserWithRole) => { + return ( + + ); + }; + + useEffect(() => setSelectedUsers([]), [isAdding]); + + return ( + <> + + {entity.label} | EnCoach + + + + + + {user && ( + +
+
+
+
+ + + +

{entity.label}

+
+
+ {allowEntityEdit && !isAdding && ( +
+ + +
+ )} +
+ +
+ Members ({users.length}) + {allowEntityEdit && !isAdding && ( +
+ + +
+ )} + {allowEntityEdit && isAdding && ( +
+ + +
+ )} +
+ + + list={isAdding ? linkedUsers : users} + renderCard={renderCard} + searchFields={[["name"], ["corporateInformation", "companyInformation", "name"], ["role", "label"], ["type"]]} + /> +
+
+ )} + + ); +} diff --git a/src/pages/entities/[id]/settings.tsx b/src/pages/entities/[id]/settings.tsx new file mode 100644 index 00000000..bce4dab7 --- /dev/null +++ b/src/pages/entities/[id]/settings.tsx @@ -0,0 +1,232 @@ +/* eslint-disable @next/next/no-img-element */ +import CardList from "@/components/High/CardList"; +import Layout from "@/components/High/Layout"; +import Tooltip from "@/components/Low/Tooltip"; +import {useListSearch} from "@/hooks/useListSearch"; +import usePagination from "@/hooks/usePagination"; +import {Entity, EntityWithRoles, Role} from "@/interfaces/entity"; +import {GroupWithUsers, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {USER_TYPE_LABELS} from "@/resources/user"; +import {getEntityWithRoles} from "@/utils/entities.be"; +import {convertToUsers, getGroup} from "@/utils/groups.be"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import {getUserName} from "@/utils/users"; +import {getEntityUsers, getLinkedUsers, getSpecificUsers} from "@/utils/users.be"; +import axios from "axios"; +import clsx from "clsx"; +import {withIronSessionSsr} from "iron-session/next"; +import moment from "moment"; +import Head from "next/head"; +import Link from "next/link"; +import {useRouter} from "next/router"; +import {Divider} from "primereact/divider"; +import {useEffect, useMemo, useState} from "react"; +import { + BsChevronLeft, + BsClockFill, + BsEnvelopeFill, + BsFillPersonVcardFill, + BsPlus, + BsSquare, + BsStopwatchFill, + BsTag, + BsTrash, + BsX, +} from "react-icons/bs"; +import {toast, ToastContainer} from "react-toastify"; + +export const getServerSideProps = withIronSessionSsr(async ({req, params}) => { + const user = req.session.user as User; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const {id} = params as {id: string}; + + const entityWithRoles = await getEntityWithRoles(id); + if (!entityWithRoles || (checkAccess(user, getTypesOfUser(["admin", "developer"])) && !user.entities.map((x) => x.id).includes(id))) { + return { + redirect: { + destination: "/entities", + permanent: false, + }, + }; + } + + const {entity, roles} = entityWithRoles; + + const linkedUsers = await getLinkedUsers(user.id, user.type); + const users = await getEntityUsers(id); + + return { + props: { + user, + entity: JSON.parse(JSON.stringify(entity)), + roles: JSON.parse(JSON.stringify(roles)), + users: JSON.parse(JSON.stringify(users)), + linkedUsers: JSON.parse(JSON.stringify(linkedUsers.users)), + }, + }; +}, sessionOptions); + +interface Props { + user: User; + entity: Entity; + roles: Role[]; + users: User[]; + linkedUsers: User[]; +} + +export default function Home({user, entity, roles, users, linkedUsers}: Props) { + const [isEditing, setIsEditing] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const router = useRouter(); + + const allowEntityEdit = useMemo(() => checkAccess(user, ["admin", "developer"]), [user]); + + const renameGroup = () => { + if (!allowEntityEdit) return; + + const name = prompt("Rename this entity:", entity.label); + if (!name) return; + + setIsLoading(true); + axios + .patch(`/api/entities/${entity.id}`, {name}) + .then(() => { + toast.success("The entity has been updated successfully!"); + router.replace(router.asPath); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const deleteGroup = () => { + if (!allowEntityEdit) return; + if (!confirm("Are you sure you want to delete this entity?")) return; + + setIsLoading(true); + + axios + .delete(`/api/entities/${entity.id}`) + .then(() => { + toast.success("This entity has been successfully deleted!"); + router.replace("/entities"); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const firstCard = () => ( + + + Create Role + + ); + + const renderCard = (role: Role) => { + const usersWithRole = users.filter((x) => x.entities.map((x) => x.role).includes(role.id)); + + return ( + + ); + }; + + return ( + <> + + {entity.label} | EnCoach + + + + + + {user && ( + +
+
+
+
+ + + +

{entity.label}

+
+
+ {allowEntityEdit && !isEditing && ( +
+ + +
+ )} +
+ + Roles + + +
+
+ )} + + ); +} diff --git a/src/pages/entities/index.tsx b/src/pages/entities/index.tsx new file mode 100644 index 00000000..00b12b47 --- /dev/null +++ b/src/pages/entities/index.tsx @@ -0,0 +1,116 @@ +/* eslint-disable @next/next/no-img-element */ +import Head from "next/head"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {ToastContainer} from "react-toastify"; +import Layout from "@/components/High/Layout"; +import {GroupWithUsers, User} from "@/interfaces/user"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {getUserName} from "@/utils/users"; +import {convertToUsers, getGroupsForUser} from "@/utils/groups.be"; +import {countEntityUsers, getEntityUsers, getSpecificUsers} from "@/utils/users.be"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import Link from "next/link"; +import {uniq} from "lodash"; +import {BsPlus} from "react-icons/bs"; +import CardList from "@/components/High/CardList"; +import {getEntitiesWithRoles} from "@/utils/entities.be"; +import {EntityWithRoles} from "@/interfaces/entity"; +import Separator from "@/components/Low/Separator"; + +type EntitiesWithCount = {entity: EntityWithRoles; users: User[]; count: number}; + +export const getServerSideProps = withIronSessionSsr(async ({req}) => { + const user = req.session.user; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const entities = await getEntitiesWithRoles( + checkAccess(user, getTypesOfUser(["admin", "developer"])) ? user.entities.map((x) => x.id) : undefined, + ); + + const entitiesWithCount = await Promise.all( + entities.map(async (e) => ({entity: e, count: await countEntityUsers(e.id), users: await getEntityUsers(e.id, 5)})), + ); + + return { + props: {user, entities: JSON.parse(JSON.stringify(entitiesWithCount))}, + }; +}, sessionOptions); + +const SEARCH_FIELDS: string[][] = [["entity", "label"]]; + +interface Props { + user: User; + entities: EntitiesWithCount[]; +} +export default function Home({user, entities}: Props) { + const renderCard = ({entity, users, count}: EntitiesWithCount) => ( + + + Entity: + {entity.label} + + Members ({count}): + + {users.map(getUserName).join(", ")} + {count > 5 ? and {count - 5} more : ""} + + + ); + + const firstCard = () => ( + + + Create Entity + + ); + + return ( + <> + + Entities | EnCoach + + + + + + {user && ( + +
+
+

Entities

+ +
+ + list={entities} searchFields={SEARCH_FIELDS} renderCard={renderCard} firstCard={firstCard} /> +
+
+ )} + + ); +} diff --git a/src/pages/groups/[id].tsx b/src/pages/groups/[id].tsx index 92592e0e..5a1348a6 100644 --- a/src/pages/groups/[id].tsx +++ b/src/pages/groups/[id].tsx @@ -78,7 +78,7 @@ export default function Home({user, group, users}: Props) { 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], + [users, group.participants, group.admin.id, user.id], ); const {rows, renderSearch} = useListSearch( @@ -122,7 +122,6 @@ export default function Home({user, group, users}: Props) { setIsLoading(true); - console.log([...group.participants.map((x) => x.id), selectedUsers]); axios .patch(`/api/groups/${group.id}`, {participants: [...group.participants.map((x) => x.id), ...selectedUsers]}) .then(() => { diff --git a/src/pages/groups/index.tsx b/src/pages/groups/index.tsx index 4199a464..1f48ac19 100644 --- a/src/pages/groups/index.tsx +++ b/src/pages/groups/index.tsx @@ -10,11 +10,10 @@ import {getUserName} from "@/utils/users"; import {convertToUsers, getGroupsForUser} from "@/utils/groups.be"; import {getSpecificUsers} from "@/utils/users.be"; import {checkAccess} from "@/utils/permissions"; -import usePagination from "@/hooks/usePagination"; -import {useListSearch} from "@/hooks/useListSearch"; import Link from "next/link"; import {uniq} from "lodash"; import {BsPlus} from "react-icons/bs"; +import CardList from "@/components/High/CardList"; import Separator from "@/components/Low/Separator"; export const getServerSideProps = withIronSessionSsr(async ({req}) => { @@ -51,24 +50,50 @@ export const getServerSideProps = withIronSessionSsr(async ({req}) => { }; }, sessionOptions); +const SEARCH_FIELDS = [ + ["name"], + ["admin", "name"], + ["admin", "email"], + ["admin", "corporateInformation", "companyInformation", "name"], + ["participants", "name"], + ["participants", "email"], + ["participants", "corporateInformation", "companyInformation", "name"], +]; + interface Props { user: User; groups: GroupWithUsers[]; } export default function Home({user, groups}: Props) { - const {rows, renderSearch} = useListSearch( - [ - ["name"], - ["admin", "name"], - ["admin", "email"], - ["admin", "corporateInformation", "companyInformation", "name"], - ["participants", "name"], - ["participants", "email"], - ["participants", "corporateInformation", "companyInformation", "name"], - ], - groups, + const renderCard = (group: GroupWithUsers) => ( + + + Group: + {group.name} + + + Admin: + {getUserName(group.admin)} + + Participants ({group.participants.length}): + + {group.participants.slice(0, 5).map(getUserName).join(", ")} + {group.participants.length > 5 ? and {group.participants.length - 5} more : ""} + + + ); + + const firstCard = () => ( + + + Create Group + ); - const {items, page, renderMinimal} = usePagination(rows, 20); return ( <> @@ -84,50 +109,13 @@ export default function Home({user, groups}: Props) { {user && ( -
-

Groups

- -
+
+
+

Classrooms

+ +
-
-
- {renderSearch()} - {renderMinimal()} -
-
- {page === 0 && ( - - - Create Group - - )} - {items.map((group) => ( - - - Group: - {group.name} - - - Admin: - {getUserName(group.admin)} - - Participants ({group.participants.length}): - - {group.participants.slice(0, 5).map(getUserName).join(", ")} - {group.participants.length > 5 ? ( - and {group.participants.length - 5} more - ) : ( - "" - )} - - - ))} -
+ list={groups} searchFields={SEARCH_FIELDS} renderCard={renderCard} firstCard={firstCard} />
)} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6a4f9ca4..cc872198 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -37,14 +37,7 @@ import {getUserCorporate} from "@/utils/groups.be"; import {getUsers} from "@/utils/users.be"; export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { - const user = req.session.user; - - const envVariables: {[key: string]: string} = {}; - Object.keys(process.env) - .filter((x) => x.startsWith("NEXT_PUBLIC")) - .forEach((x: string) => { - envVariables[x] = process.env[x]!; - }); + const user = req.session.user as User | undefined; if (!user) { return { @@ -58,13 +51,12 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { const linkedCorporate = (await getUserCorporate(user.id)) || null; return { - props: {user, envVariables, linkedCorporate}, + props: {user, linkedCorporate}, }; }, sessionOptions); interface Props { user: User; - envVariables: {[key: string]: string}; linkedCorporate?: CorporateUser | MasterCorporateUser; } diff --git a/src/utils/assignments.be.ts b/src/utils/assignments.be.ts index 261f04b6..5c0bb9d5 100644 --- a/src/utils/assignments.be.ts +++ b/src/utils/assignments.be.ts @@ -18,10 +18,18 @@ export const getAssignmentsByAssigner = async (id: string, startDate?: Date, end return await db.collection("assignments").find(query).toArray(); }; + export const getAssignments = async () => { return await db.collection("assignments").find({}).toArray(); }; +export const getAssignmentsByAssignee = async (id: string, filter?: {[key in keyof Partial]: any}) => { + return await db + .collection("assignments") + .find({assignees: [id], ...(!filter ? {} : filter)}) + .toArray(); +}; + export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => { return await db.collection("assignments").find({assigner: id}).toArray(); }; @@ -37,6 +45,17 @@ export const getAssignmentsByAssigners = async (ids: string[], startDate?: Date, .toArray(); }; +export const getEntityAssignments = async (id: string) => { + return await db.collection("assignments").find({entity: id}).toArray(); +}; + +export const getEntitiesAssignments = async (ids: string[]) => { + return await db + .collection("assignments") + .find({entity: {$in: ids}}) + .toArray(); +}; + 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/entities.be.ts b/src/utils/entities.be.ts new file mode 100644 index 00000000..74604011 --- /dev/null +++ b/src/utils/entities.be.ts @@ -0,0 +1,35 @@ +import {Entity, EntityWithRoles, Role} from "@/interfaces/entity"; +import client from "@/lib/mongodb"; +import {getRolesByEntities, getRolesByEntity} from "./roles.be"; + +const db = client.db(process.env.MONGODB_DB); + +export const getEntityWithRoles = async (id: string): Promise<{entity: Entity; roles: Role[]} | undefined> => { + const entity = await getEntity(id); + if (!entity) return undefined; + + const roles = await getRolesByEntity(id); + return {entity, roles}; +}; + +export const getEntity = async (id: string) => { + 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}} : {}) + .toArray(); + + const roles = await getRolesByEntities(entities.map((x) => 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}} : {}) + .toArray(); +}; diff --git a/src/utils/exams.be.ts b/src/utils/exams.be.ts index 70bcd685..f9686dc2 100644 --- a/src/utils/exams.be.ts +++ b/src/utils/exams.be.ts @@ -1,5 +1,5 @@ import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and} from "firebase/firestore"; -import {shuffle} from "lodash"; +import {groupBy, shuffle} from "lodash"; import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam"; import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user"; import {Module} from "@/interfaces"; @@ -29,6 +29,23 @@ export async function getSpecificExams(ids: string[]) { return exams; } +export const getExamsByIds = async (ids: {module: Module; id: string}[]) => { + const groupedByModule = groupBy(ids, "module"); + const exams: Exam[] = ( + await Promise.all( + Object.keys(groupedByModule).map( + async (m) => + await db + .collection(m) + .find({id: {$in: groupedByModule[m]}}) + .toArray(), + ), + ) + ).flat(); + + return exams; +}; + export const getExams = async ( db: Db, module: Module, diff --git a/src/utils/grading.be.ts b/src/utils/grading.be.ts index 391d1ed3..1c7742f5 100644 --- a/src/utils/grading.be.ts +++ b/src/utils/grading.be.ts @@ -20,3 +20,6 @@ export const getGradingSystem = async (user: User): Promise => { return {steps: CEFR_STEPS, user: user.id}; }; + +export const getGradingSystemByEntity = async (id: string) => + (await db.collection("grading").findOne({entity: id})) || {steps: CEFR_STEPS, user: ""}; diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index f500311b..ab55c9fb 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -116,3 +116,11 @@ export const getCorporateNameForStudent = async (studentID: string) => { return ""; }; + +export const getGroupsByEntity = async (id: string) => await db.collection("groups").find({entity: id}).toArray(); + +export const getGroupsByEntities = async (ids: string[]) => + await db + .collection("groups") + .find({entity: {$in: ids}}) + .toArray(); diff --git a/src/utils/index.ts b/src/utils/index.ts index 892cafbc..f0862650 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -43,3 +43,11 @@ export const convertBase64 = (file: File) => { }; }); }; + +export const mapBy = (obj: T[], key: keyof T) => { + if (!obj) return undefined; + return obj.map((i) => i[key]); +}; +export const filterBy = (obj: T[], key: keyof T, value: any) => obj.filter((i) => i[key] === value); + +export const serialize = (obj: T): T => JSON.parse(JSON.stringify(obj)); diff --git a/src/utils/invites.be.ts b/src/utils/invites.be.ts new file mode 100644 index 00000000..ad68ae0c --- /dev/null +++ b/src/utils/invites.be.ts @@ -0,0 +1,18 @@ +import {Session} from "@/hooks/useSessions"; +import {Invite, InviteWithUsers} from "@/interfaces/invite"; +import {User} from "@/interfaces/user"; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); + +export const getInvitesByInvitee = async (id: string, limit?: number) => + await db + .collection("invites") + .find({to: id}) + .limit(limit || 0) + .toArray(); + +export const convertInvitersToUsers = async (invite: Invite): Promise => ({ + ...invite, + from: (await db.collection("users").findOne({id: invite.from})) ?? undefined, +}); diff --git a/src/utils/roles.be.ts b/src/utils/roles.be.ts new file mode 100644 index 00000000..d0114b74 --- /dev/null +++ b/src/utils/roles.be.ts @@ -0,0 +1,14 @@ +import {Role} from "@/interfaces/entity"; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); + +export const getRolesByEntities = async (entityIDs: string[]) => + await db + .collection("roles") + .find({entityID: {$in: entityIDs}}) + .toArray(); + +export const getRolesByEntity = async (entityID: string) => await db.collection("roles").find({entityID}).toArray(); + +export const getRole = async (id: string) => (await db.collection("roles").findOne({id})) ?? undefined; diff --git a/src/utils/sessions.be.ts b/src/utils/sessions.be.ts new file mode 100644 index 00000000..a6fbe357 --- /dev/null +++ b/src/utils/sessions.be.ts @@ -0,0 +1,11 @@ +import {Session} from "@/hooks/useSessions"; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); + +export const getSessionsByUser = async (id: string, limit?: number) => + await db + .collection("sessions") + .find({user: id}) + .limit(limit || 0) + .toArray(); diff --git a/src/utils/stats.be.ts b/src/utils/stats.be.ts new file mode 100644 index 00000000..4abc852e --- /dev/null +++ b/src/utils/stats.be.ts @@ -0,0 +1,12 @@ +import {Stat} from "@/interfaces/user"; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); + +export const getStatsByUser = async (id: string) => await db.collection("stats").find({user: id}).toArray(); + +export const getStatsByUsers = async (ids: string[]) => + await db + .collection("stats") + .find({user: {$in: ids}}) + .toArray(); diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index caee2762..963eef79 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -1,9 +1,11 @@ -import {CorporateUser, Group, Type, User} from "@/interfaces/user"; +import {CorporateUser, Type, User} from "@/interfaces/user"; import {getGroupsForUser, getParticipantGroups, getUserGroups, getUsersGroups} from "./groups.be"; -import {last, uniq, uniqBy} from "lodash"; +import {uniq} from "lodash"; import {getUserCodes} from "./codes.be"; -import moment from "moment"; import client from "@/lib/mongodb"; +import {WithEntity} from "@/interfaces/entity"; +import {getEntity} from "./entities.be"; +import {getRole} from "./roles.be"; const db = client.db(process.env.MONGODB_DB); @@ -14,6 +16,22 @@ export async function getUsers() { .toArray(); } +export async function getUserWithEntity(id: string): Promise | undefined> { + const user = await db.collection("users").findOne({id: id}, {projection: {_id: 0}}); + if (!user) return undefined; + + const entities = await Promise.all( + user.entities.map(async (e) => { + const entity = await getEntity(e.id); + const role = await getRole(e.role); + + return {entity, role}; + }), + ); + + return {...user, entities}; +} + export async function getUser(id: string): Promise { const user = await db.collection("users").findOne({id: id}, {projection: {_id: 0}}); return !!user ? user : undefined; @@ -28,6 +46,30 @@ export async function getSpecificUsers(ids: string[]) { .toArray(); } +export async function getEntityUsers(id: string, limit?: number) { + return await db + .collection("users") + .find({"entities.id": id}) + .limit(limit || 0) + .toArray(); +} + +export async function countEntityUsers(id: string) { + return await db.collection("users").countDocuments({"entities.id": id}); +} + +export async function getEntitiesUsers(ids: string[], limit?: number) { + return await db + .collection("users") + .find({"entities.id": {$in: ids}}) + .limit(limit || 0) + .toArray(); +} + +export async function countEntitiesUsers(ids: string[]) { + return await db.collection("users").countDocuments({"entities.id": {$in: ids}}); +} + export async function getLinkedUsers( userID?: string, userType?: Type,