From 1ef4efcacfd7e1498e07c869be0645e37808d816 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 7 Oct 2024 15:49:58 +0100 Subject: [PATCH] Continued updating the code to work with entities better --- src/components/High/CardList.tsx | 12 +- src/components/High/Table.tsx | 107 ++ src/components/Medium/RecordFilter.tsx | 96 +- src/components/UserDisplayList.tsx | 30 + src/hooks/useEntities.tsx | 23 + src/hooks/useEntitiesGroups.tsx | 23 + src/hooks/useEntitiesUsers.tsx | 23 + src/interfaces/entity.ts | 31 +- src/pages/(admin)/Lists/GroupList.tsx | 599 +++++------ src/pages/(admin)/Lists/UserList.tsx | 984 +++++++----------- src/pages/(admin)/UserCreator.tsx | 481 +++++---- src/pages/api/batch_users.ts | 93 +- src/pages/api/entities/groups.ts | 32 + src/pages/api/entities/users.ts | 47 + src/pages/api/make_user.ts | 295 ++---- src/pages/api/user.ts | 26 +- src/pages/dashboard/admin.tsx | 195 ++++ src/pages/dashboard/admin/index.tsx | 225 ---- .../{corporate/index.tsx => corporate.tsx} | 62 +- src/pages/dashboard/developer.tsx | 201 ++++ src/pages/dashboard/developer/index.tsx | 225 ---- .../index.tsx => mastercorporate.tsx} | 64 +- .../{student/index.tsx => student.tsx} | 0 src/pages/dashboard/teacher.tsx | 170 +++ src/pages/dashboard/teacher/index.tsx | 184 ---- src/pages/list/users.tsx | 44 +- src/pages/lists/users.tsx | 259 ----- src/pages/record.tsx | 142 +-- src/pages/settings.tsx | 257 ++--- src/pages/stats.tsx | 61 +- src/pages/training/index.tsx | 20 +- src/resources/speakingAvatars.ts | 46 +- src/utils/groups.be.ts | 183 ++-- src/utils/index.ts | 2 +- src/utils/users.be.ts | 188 ++-- src/utils/users.ts | 71 +- 36 files changed, 2489 insertions(+), 3012 deletions(-) create mode 100644 src/components/High/Table.tsx create mode 100644 src/components/UserDisplayList.tsx create mode 100644 src/hooks/useEntities.tsx create mode 100644 src/hooks/useEntitiesGroups.tsx create mode 100644 src/hooks/useEntitiesUsers.tsx create mode 100644 src/pages/api/entities/groups.ts create mode 100644 src/pages/api/entities/users.ts create mode 100644 src/pages/dashboard/admin.tsx delete mode 100644 src/pages/dashboard/admin/index.tsx rename src/pages/dashboard/{corporate/index.tsx => corporate.tsx} (77%) create mode 100644 src/pages/dashboard/developer.tsx delete mode 100644 src/pages/dashboard/developer/index.tsx rename src/pages/dashboard/{mastercorporate/index.tsx => mastercorporate.tsx} (74%) rename src/pages/dashboard/{student/index.tsx => student.tsx} (100%) create mode 100644 src/pages/dashboard/teacher.tsx delete mode 100644 src/pages/dashboard/teacher/index.tsx delete mode 100644 src/pages/lists/users.tsx diff --git a/src/components/High/CardList.tsx b/src/components/High/CardList.tsx index d54e7107..31075dcd 100644 --- a/src/components/High/CardList.tsx +++ b/src/components/High/CardList.tsx @@ -1,5 +1,6 @@ import {useListSearch} from "@/hooks/useListSearch"; import usePagination from "@/hooks/usePagination"; +import { clsx } from "clsx"; import {ReactNode} from "react"; import Checkbox from "../Low/Checkbox"; import Separator from "../Low/Separator"; @@ -10,20 +11,21 @@ interface Props { pageSize?: number; firstCard?: () => ReactNode; renderCard: (item: T) => ReactNode; + className?: string } -export default function CardList({list, searchFields, renderCard, firstCard, pageSize = 20}: Props) { +export default function CardList({list, searchFields, renderCard, firstCard, className, pageSize = 20}: Props) { const {rows, renderSearch} = useListSearch(searchFields, list); - const {items, page, renderMinimal} = usePagination(rows, pageSize); + const {items, page, render, renderMinimal} = usePagination(rows, pageSize); return (
- {renderSearch()} - {renderMinimal()} + {searchFields.length > 0 && renderSearch()} + {searchFields.length > 0 ? renderMinimal() : render()}
-
+
{page === 0 && !!firstCard && firstCard()} {items.map(renderCard)}
diff --git a/src/components/High/Table.tsx b/src/components/High/Table.tsx new file mode 100644 index 00000000..c5b01e78 --- /dev/null +++ b/src/components/High/Table.tsx @@ -0,0 +1,107 @@ +import { useListSearch } from "@/hooks/useListSearch" +import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, PaginationState, useReactTable } from "@tanstack/react-table" +import clsx from "clsx" +import { useState } from "react" +import { BsArrowDown, BsArrowUp } from "react-icons/bs" +import Button from "../Low/Button" + +interface Props { + data: T[] + columns: ColumnDef[] + searchFields: string[][] + size?: number + onDownload?: (rows: T[]) => void +} + +export default function Table({ data, columns, searchFields, size = 16, onDownload }: Props) { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 16, + }) + + const { rows, renderSearch } = useListSearch(searchFields, data); + + const table = useReactTable({ + data: rows, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + state: { + pagination + } + }); + + return ( +
+
+ {renderSearch()} + {onDownload && ( + + ) + } +
+ +
+
+ +
+
+ +
Page
+ + {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount().toLocaleString()} + +
| Total: {table.getRowCount().toLocaleString()}
+
+ +
+
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ) +} diff --git a/src/components/Medium/RecordFilter.tsx b/src/components/Medium/RecordFilter.tsx index c70882c7..a8e5ce89 100644 --- a/src/components/Medium/RecordFilter.tsx +++ b/src/components/Medium/RecordFilter.tsx @@ -1,11 +1,13 @@ import { User } from "@/interfaces/user"; import { checkAccess } from "@/utils/permissions"; import Select from "../Low/Select"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useEffect, useMemo, useState } from "react"; import clsx from "clsx"; import useUsers from "@/hooks/useUsers"; import useGroups from "@/hooks/useGroups"; import useRecordStore from "@/stores/recordStore"; +import { EntityWithRoles } from "@/interfaces/entity"; +import { mapBy } from "@/utils"; type TimeFilter = "months" | "weeks" | "days"; @@ -13,6 +15,8 @@ type Filter = TimeFilter | "assignments" | undefined; interface Props { user: User; + entities: EntityWithRoles[] + users: User[] filterState: { filter: Filter, setFilter: React.Dispatch> @@ -28,83 +32,41 @@ const defaultSelectableCorporate = { const RecordFilter: React.FC = ({ user, + entities, + users, filterState, assignments = true, children }) => { const { filter, setFilter } = filterState; - const [statsUserId, setStatsUserId] = useRecordStore((state) => [ + const [entity, setEntity] = useState() + + const [, setStatsUserId] = useRecordStore((state) => [ state.selectedUser, state.setSelectedUser ]); - const { users } = useUsers(); - const { groups: allGroups } = useGroups({}); - const { groups } = useGroups({ admin: user?.id, userType: user?.type }); + const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [users, entity]) + + useEffect(() => setStatsUserId(user.id), [setStatsUserId, user.id]) const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => { setFilter((prev) => (prev === value ? undefined : value)); }; - const selectableCorporates = [ - defaultSelectableCorporate, - ...users - .filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id)) - .filter((x) => x.type === "corporate") - .map((x) => ({ - value: x.id, - label: `${x.name} - ${x.email}`, - })), - ]; - - const [selectedCorporate, setSelectedCorporate] = useState(defaultSelectableCorporate.value); - - const getUsersList = (): User[] => { - if (selectedCorporate) { - const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate); - const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants); - - const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[]; - return userListWithUsers.filter((x) => x); - } - - return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id)); - }; - - const corporateFilteredUserList = getUsersList(); - - const getSelectedUser = () => { - if (selectedCorporate) { - const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId); - return userInCorporate || corporateFilteredUserList[0]; - } - - return users.find((x) => x.id === statsUserId) || user; - }; - - const selectedUser = getSelectedUser(); - const selectedUserSelectValue = selectedUser - ? { - value: selectedUser.id, - label: `${selectedUser.name} - ${selectedUser.email}`, - } - : { - value: "", - label: "", - }; - return (
-
+
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && ( <> - +
+ + }} /> +
+
groups.flatMap((y) => y.participants).includes(x.id)) .map((x) => ({ value: x.id, label: `${x.name} - ${x.email}`, }))} - value={selectedUserSelectValue} + defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} onChange={(value) => setStatsUserId(value?.value!)} styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }), @@ -155,7 +119,7 @@ const RecordFilter: React.FC = ({ }), }} /> - +
)} {children}
@@ -203,4 +167,4 @@ const RecordFilter: React.FC = ({ ); } -export default RecordFilter; \ No newline at end of file +export default RecordFilter; diff --git a/src/components/UserDisplayList.tsx b/src/components/UserDisplayList.tsx new file mode 100644 index 00000000..8f1e803f --- /dev/null +++ b/src/components/UserDisplayList.tsx @@ -0,0 +1,30 @@ +/** eslint-disable @next/next/no-img-element */ +import { User } from "@/interfaces/user" + +interface Props { + users: User[] + title: string; +} + +const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+); + +export default function UserDisplayList({ title, users }: Props) { + return (
+ {title} +
+ {users + .slice(0, 10) + .map((x) => ( + + ))} +
+
) +} diff --git a/src/hooks/useEntities.tsx b/src/hooks/useEntities.tsx new file mode 100644 index 00000000..0999363f --- /dev/null +++ b/src/hooks/useEntities.tsx @@ -0,0 +1,23 @@ +import { EntityWithRoles } from "@/interfaces/entity"; +import { Discount } from "@/interfaces/paypal"; +import { Code, Group, User } from "@/interfaces/user"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +export default function useEntities(creator?: string) { + const [entities, setEntities] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get("/api/entities?showRoles=true") + .then((response) => setEntities(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [creator]); + + return { entities, isLoading, isError, reload: getData }; +} diff --git a/src/hooks/useEntitiesGroups.tsx b/src/hooks/useEntitiesGroups.tsx new file mode 100644 index 00000000..f0bd565c --- /dev/null +++ b/src/hooks/useEntitiesGroups.tsx @@ -0,0 +1,23 @@ +import { EntityWithRoles, WithEntity, WithLabeledEntities } from "@/interfaces/entity"; +import { Discount } from "@/interfaces/paypal"; +import { Code, Group, Type, User } from "@/interfaces/user"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +export default function useEntitiesGroups() { + const [groups, setGroups] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get[]>(`/api/entities/groups`) + .then((response) => setGroups(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, []); + + return { groups, isLoading, isError, reload: getData }; +} diff --git a/src/hooks/useEntitiesUsers.tsx b/src/hooks/useEntitiesUsers.tsx new file mode 100644 index 00000000..f1c7be6a --- /dev/null +++ b/src/hooks/useEntitiesUsers.tsx @@ -0,0 +1,23 @@ +import { EntityWithRoles, WithLabeledEntities } from "@/interfaces/entity"; +import { Discount } from "@/interfaces/paypal"; +import { Code, Group, Type, User } from "@/interfaces/user"; +import axios from "axios"; +import { useEffect, useState } from "react"; + +export default function useEntitiesUsers(type?: Type) { + const [users, setUsers] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get[]>(`/api/entities/users${type ? "?type=" + type : ""}`) + .then((response) => setUsers(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [type]); + + return { users, isLoading, isError, reload: getData }; +} diff --git a/src/interfaces/entity.ts b/src/interfaces/entity.ts index 071e031a..c55ee793 100644 --- a/src/interfaces/entity.ts +++ b/src/interfaces/entity.ts @@ -1,19 +1,28 @@ export interface Entity { - id: string; - label: string; + id: string; + label: string; } export interface Role { - id: string; - entityID: string; - permissions: string[]; - label: string; + id: string; + entityID: string; + permissions: string[]; + label: string; } export interface EntityWithRoles extends Entity { - roles: Role[]; -} + roles: Role[]; +}; -export type WithEntity = T extends {entities: {id: string; role: string}[]} - ? Omit & {entities: {entity?: Entity; role?: Role}[]} - : T; +export type WithLabeledEntities = T extends { entities: { id: string; role: string }[] } + ? Omit & { entities: { id: string; label?: string; role: string, roleLabel?: string }[] } + : T; + + +export type WithEntity = T extends { entity?: string } + ? Omit & { entity: Entity } + : T; + +export type WithEntities = T extends { entities: { id: string; role: string }[] } + ? Omit & { entities: { entity?: Entity; role?: Role }[] } + : T; diff --git a/src/pages/(admin)/Lists/GroupList.tsx b/src/pages/(admin)/Lists/GroupList.tsx index 993f56da..46426014 100644 --- a/src/pages/(admin)/Lists/GroupList.tsx +++ b/src/pages/(admin)/Lists/GroupList.tsx @@ -3,373 +3,300 @@ import Input from "@/components/Low/Input"; import Modal from "@/components/Modal"; import useGroups from "@/hooks/useGroups"; import useUsers from "@/hooks/useUsers"; -import {CorporateUser, Group, User} from "@/interfaces/user"; -import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import { CorporateUser, Group, User } from "@/interfaces/user"; +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import axios from "axios"; -import {capitalize, uniq} from "lodash"; -import {useEffect, useMemo, useState} from "react"; -import {BsPencil, BsQuestionCircleFill, BsTrash} from "react-icons/bs"; +import { capitalize, uniq } from "lodash"; +import { useEffect, useMemo, useState } from "react"; +import { BsPencil, BsQuestionCircleFill, BsTrash } from "react-icons/bs"; import Select from "react-select"; -import {toast} from "react-toastify"; +import { toast } from "react-toastify"; import readXlsxFile from "read-excel-file"; -import {useFilePicker} from "use-file-picker"; -import {getUserCorporate} from "@/utils/groups"; -import {isAgentUser, isCorporateUser, USER_TYPE_LABELS} from "@/resources/user"; -import {checkAccess} from "@/utils/permissions"; +import { useFilePicker } from "use-file-picker"; +import { getUserCorporate } from "@/utils/groups"; +import { isAgentUser, isCorporateUser, USER_TYPE_LABELS } from "@/resources/user"; +import { checkAccess } from "@/utils/permissions"; import usePermissions from "@/hooks/usePermissions"; -import {useListSearch} from "@/hooks/useListSearch"; +import { useListSearch } from "@/hooks/useListSearch"; +import Table from "@/components/High/Table"; +import useEntitiesGroups from "@/hooks/useEntitiesGroups"; +import useEntitiesUsers from "@/hooks/useEntitiesUsers"; +import { WithEntity } from "@/interfaces/entity"; const searchFields = [["name"]]; -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper>(); const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); -const LinkedCorporate = ({userId, users, groups}: {userId: string; users: User[]; groups: Group[]}) => { - const [companyName, setCompanyName] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const user = users.find((u) => u.id === userId); - if (!user) return setCompanyName(""); - - if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name); - if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name); - - const belongingGroups = groups.filter((x) => x.participants.includes(userId)); - const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x)); - - if (belongingGroupsAdmins.length === 0) return setCompanyName(""); - - const admin = belongingGroupsAdmins[0] as CorporateUser; - setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name); - }, [userId, users, groups]); - - return isLoading ? Loading... : <>{companyName}; -}; - interface CreateDialogProps { - user: User; - users: User[]; - group?: Group; - onClose: () => void; + user: User; + users: User[]; + group?: Group; + onClose: () => void; } -const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => { - const [name, setName] = useState(group?.name || undefined); - const [admin, setAdmin] = useState(group?.admin || user.id); - const [participants, setParticipants] = useState(group?.participants || []); - const [isLoading, setIsLoading] = useState(false); +const CreatePanel = ({ user, users, group, onClose }: CreateDialogProps) => { + const [name, setName] = useState(group?.name || undefined); + const [admin, setAdmin] = useState(group?.admin || user.id); + const [participants, setParticipants] = useState(group?.participants || []); + const [isLoading, setIsLoading] = useState(false); - const {openFilePicker, filesContent, clear} = useFilePicker({ - accept: ".xlsx", - multiple: false, - readAs: "ArrayBuffer", - }); + const { openFilePicker, filesContent, clear } = useFilePicker({ + accept: ".xlsx", + multiple: false, + readAs: "ArrayBuffer", + }); - const availableUsers = useMemo(() => { - if (user.type === "teacher") return users.filter((x) => ["student"].includes(x.type)); - if (user.type === "corporate") return users.filter((x) => ["teacher", "student"].includes(x.type)); - if (user.type === "mastercorporate") return users.filter((x) => ["corporate", "teacher", "student"].includes(x.type)); + const availableUsers = useMemo(() => { + if (user.type === "teacher") return users.filter((x) => ["student"].includes(x.type)); + if (user.type === "corporate") return users.filter((x) => ["teacher", "student"].includes(x.type)); + if (user.type === "mastercorporate") return users.filter((x) => ["corporate", "teacher", "student"].includes(x.type)); - return users; - }, [user, users]); + return users; + }, [user, users]); - useEffect(() => { - if (filesContent.length > 0) { - setIsLoading(true); + useEffect(() => { + if (filesContent.length > 0) { + setIsLoading(true); - const file = filesContent[0]; - readXlsxFile(file.content).then((rows) => { - const emails = uniq( - rows - .map((row) => { - const [email] = row as string[]; - return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined; - }) - .filter((x) => !!x), - ); + const file = filesContent[0]; + readXlsxFile(file.content).then((rows) => { + const emails = uniq( + rows + .map((row) => { + const [email] = row as string[]; + return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined; + }) + .filter((x) => !!x), + ); - if (emails.length === 0) { - toast.error("Please upload an Excel file containing e-mails!"); - clear(); - setIsLoading(false); - return; - } + if (emails.length === 0) { + toast.error("Please upload an Excel file containing e-mails!"); + clear(); + setIsLoading(false); + return; + } - const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); - const filteredUsers = emailUsers.filter( - (x) => - ((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") && - (x?.type === "student" || x?.type === "teacher")) || - (user.type === "teacher" && x?.type === "student"), - ); + const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); + const filteredUsers = emailUsers.filter( + (x) => + ((user.type === "developer" || user.type === "admin" || user.type === "corporate" || user.type === "mastercorporate") && + (x?.type === "student" || x?.type === "teacher")) || + (user.type === "teacher" && x?.type === "student"), + ); - setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); - toast.success( - user.type !== "teacher" - ? "Added all teachers and students found in the file you've provided!" - : "Added all students found in the file you've provided!", - {toastId: "upload-success"}, - ); - setIsLoading(false); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filesContent, user.type, users]); + setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); + toast.success( + user.type !== "teacher" + ? "Added all teachers and students found in the file you've provided!" + : "Added all students found in the file you've provided!", + { toastId: "upload-success" }, + ); + setIsLoading(false); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filesContent, user.type, users]); - const submit = () => { - setIsLoading(true); + const submit = () => { + setIsLoading(true); - if (name !== group?.name && (name?.trim() === "Students" || name?.trim() === "Teachers" || name?.trim() === "Corporate")) { - toast.error("That group name is reserved and cannot be used, please enter another one."); - setIsLoading(false); - return; - } + if (name !== group?.name && (name?.trim() === "Students" || name?.trim() === "Teachers" || name?.trim() === "Corporate")) { + toast.error("That group name is reserved and cannot be used, please enter another one."); + setIsLoading(false); + return; + } - (group ? axios.patch : axios.post)(group ? `/api/groups/${group.id}` : "/api/groups", {name, admin, participants}) - .then(() => { - toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`); - return true; - }) - .catch(() => { - toast.error("Something went wrong, please try again later!"); - return false; - }) - .finally(() => { - setIsLoading(false); - onClose(); - }); - }; + (group ? axios.patch : axios.post)(group ? `/api/groups/${group.id}` : "/api/groups", { name, admin, participants }) + .then(() => { + toast.success(`Group "${name}" ${group ? "edited" : "created"} successfully`); + return true; + }) + .catch(() => { + toast.error("Something went wrong, please try again later!"); + return false; + }) + .finally(() => { + setIsLoading(false); + onClose(); + }); + }; - return ( -
-
- -
-
- -
- -
-
-
- +
+
+ +
+ +
+
+
+ + - return ( -
-
- - + + - - +
+ + +
-
- - -
+ - + {type === "student" && ( + <> + + + + )} - {type === "student" && ( - <> - - - - )} +
+ + ({value: u.id, label: getUserName(u)}))} - isClearable - onChange={(e) => setSelectedCorporate(e?.value || undefined)} - /> -
- )} + {["corporate", "mastercorporate"].includes(type) && ( + + )} - {["corporate", "mastercorporate"].includes(type) && ( - - )} +
+ + (!selectedCorporate ? true : x.admin === selectedCorporate)) - .map((g) => ({value: g.id, label: g.name}))} - onChange={(e) => setGroup(e?.value || undefined)} - /> -
- )} +
+ + {user && ( + + )} +
-
- - {user && ( - - )} -
+
+ {user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && ( + <> +
+ + + Enabled + +
+ {isExpiryDateEnabled && ( + + moment(date).isAfter(new Date()) && + (user?.subscriptionExpirationDate ? moment(date).isBefore(user?.subscriptionExpirationDate) : true) + } + dateFormat="dd/MM/yyyy" + selected={expiryDate} + onChange={(date) => setExpiryDate(date)} + /> + )} + + )} +
+
-
- {user && checkAccess(user, ["developer", "admin", "corporate", "mastercorporate"]) && ( - <> -
- - - Enabled - -
- {isExpiryDateEnabled && ( - - moment(date).isAfter(new Date()) && - (user?.subscriptionExpirationDate ? moment(date).isBefore(user?.subscriptionExpirationDate) : true) - } - dateFormat="dd/MM/yyyy" - selected={expiryDate} - onChange={(date) => setExpiryDate(date)} - /> - )} - - )} -
-
- - -
- ); + +
+ ); } diff --git a/src/pages/api/batch_users.ts b/src/pages/api/batch_users.ts index f54c0d49..fdd2ec7b 100644 --- a/src/pages/api/batch_users.ts +++ b/src/pages/api/batch_users.ts @@ -1,6 +1,6 @@ -import type {NextApiRequest, NextApiResponse} from "next"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; import { FirebaseScrypt } from 'firebase-scrypt'; import { firebaseAuthScryptParams } from "@/firebase"; import crypto from 'crypto'; @@ -9,53 +9,58 @@ import axios from "axios"; export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "POST") return post(req, res); + if (req.method === "POST") return post(req, res); - return res.status(404).json({ok: false}); + return res.status(404).json({ ok: false }); } async function post(req: NextApiRequest, res: NextApiResponse) { - const maker = req.session.user; - if (!maker) { - return res.status(401).json({ok: false, reason: "You must be logged in to make user!"}); - } + const maker = req.session.user; + if (!maker) { + return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" }); + } - const scrypt = new FirebaseScrypt(firebaseAuthScryptParams) + const scrypt = new FirebaseScrypt(firebaseAuthScryptParams) - const users = req.body.users as { - email: string; - name: string; - type: string; - passport_id: string; - groupName?: string; - corporate?: string; - studentID?: string; - expiryDate?: string; - demographicInformation: { - country?: string; - passport_id?: string; - phone: string; - }; - passwordHash: string | undefined; - passwordSalt: string | undefined; - }[]; + const users = req.body.users as { + email: string; + name: string; + type: string; + passport_id: string; + groupName?: string; + corporate?: string; + studentID?: string; + expiryDate?: string; + demographicInformation: { + country?: string; + passport_id?: string; + phone: string; + }; + entity?: string + entities: { id: string, role: string }[] + passwordHash: string | undefined; + passwordSalt: string | undefined; + }[]; - const usersWithPasswordHashes = await Promise.all(users.map(async (user) => { - const currentUser = { ...user }; - const salt = crypto.randomBytes(16).toString('base64'); - const hash = await scrypt.hash(user.passport_id, salt); - - currentUser.email = currentUser.email.toLowerCase(); - currentUser.passwordHash = hash; - currentUser.passwordSalt = salt; - return currentUser; - })); - - const backendRequest = await axios.post(`${process.env.BACKEND_URL}/batch_users`, { makerID: maker.id, users: usersWithPasswordHashes }, { - headers: { - Authorization: `Bearer ${process.env.BACKEND_JWT}`, - }, - }); + const usersWithPasswordHashes = await Promise.all(users.map(async (user) => { + const currentUser = { ...user }; + const salt = crypto.randomBytes(16).toString('base64'); + const hash = await scrypt.hash(user.passport_id, salt); - return res.status(backendRequest.status).json(backendRequest.data) + currentUser.entities = [{ id: currentUser.entity!, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930" }] + delete currentUser.entity + + currentUser.email = currentUser.email.toLowerCase(); + currentUser.passwordHash = hash; + currentUser.passwordSalt = salt; + return currentUser; + })); + + const backendRequest = await axios.post(`${process.env.BACKEND_URL}/batch_users`, { makerID: maker.id, users: usersWithPasswordHashes }, { + headers: { + Authorization: `Bearer ${process.env.BACKEND_JWT}`, + }, + }); + + return res.status(backendRequest.status).json(backendRequest.data) } diff --git a/src/pages/api/entities/groups.ts b/src/pages/api/entities/groups.ts new file mode 100644 index 00000000..d2d0f12f --- /dev/null +++ b/src/pages/api/entities/groups.ts @@ -0,0 +1,32 @@ +// 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, WithEntities, WithEntity, WithLabeledEntities } from "@/interfaces/entity"; +import { v4 } from "uuid"; +import { mapBy } from "@/utils"; +import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { Group, User } from "@/interfaces/user"; +import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return await get(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 groups: WithEntity[] = ["admin", "developer"].includes(user.type) + ? await getGroups() + : await getGroupsByEntities(mapBy(user.entities || [], 'id')) + + res.status(200).json(groups); +} diff --git a/src/pages/api/entities/users.ts b/src/pages/api/entities/users.ts new file mode 100644 index 00000000..a4df6d0b --- /dev/null +++ b/src/pages/api/entities/users.ts @@ -0,0 +1,47 @@ +// 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, WithEntities, WithLabeledEntities } from "@/interfaces/entity"; +import { v4 } from "uuid"; +import { mapBy } from "@/utils"; +import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { User } from "@/interfaces/user"; + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return await get(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 { type } = req.query as { type: string } + const entities = await getEntitiesWithRoles(mapBy(user.entities || [], 'id')) + + const filter = !type ? undefined : { type } + const users = ["admin", "developer"].includes(user.type) + ? await getUsers(filter) + : await getEntitiesUsers(mapBy(entities, 'id') as string[], filter) + + const usersWithEntities: WithLabeledEntities[] = users.map((u) => { + return { + ...u, entities: (u.entities || []).map((e) => { + const entity = entities.find((x) => x.id === e.id) + if (!entity) return e + + const role = entity.roles.find((x) => x.id === e.role) + return { id: e.id, label: entity.label, role: e.role, roleLabel: role?.label } + }) + } + }) + + res.status(200).json(usersWithEntities); +} diff --git a/src/pages/api/make_user.ts b/src/pages/api/make_user.ts index b8128015..dd148874 100644 --- a/src/pages/api/make_user.ts +++ b/src/pages/api/make_user.ts @@ -1,28 +1,28 @@ -import type {NextApiRequest, NextApiResponse} from "next"; -import {app} from "@/firebase"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {v4} from "uuid"; -import {CorporateUser, Group, Type, User} from "@/interfaces/user"; -import {createUserWithEmailAndPassword, getAuth} from "firebase/auth"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { v4 } from "uuid"; +import { CorporateUser, Group, Type, User } from "@/interfaces/user"; +import { createUserWithEmailAndPassword, getAuth } from "firebase/auth"; import ShortUniqueId from "short-unique-id"; -import {getGroup, getGroups, getUserCorporate, getUserGroups, getUserNamedGroup} from "@/utils/groups.be"; -import {uniq} from "lodash"; -import {getSpecificUsers, getUser} from "@/utils/users.be"; +import { getGroup, getGroups, getUserCorporate, getUserGroups, getUserNamedGroup } from "@/utils/groups.be"; +import { uniq } from "lodash"; +import { getSpecificUsers, getUser } from "@/utils/users.be"; import client from "@/lib/mongodb"; const DEFAULT_DESIRED_LEVELS = { - reading: 9, - listening: 9, - writing: 9, - speaking: 9, + reading: 9, + listening: 9, + writing: 9, + speaking: 9, }; const DEFAULT_LEVELS = { - reading: 0, - listening: 0, - writing: 0, - speaking: 0, + reading: 0, + listening: 0, + writing: 0, + speaking: 0, }; const auth = getAuth(app); @@ -30,198 +30,97 @@ const db = client.db(process.env.MONGODB_DB); export default withIronSessionApiRoute(handler, sessionOptions); -const getUsersOfType = async (admin: string, type: Type) => { - const groups = await getUserGroups(admin); - const participants = groups.flatMap((x) => x.participants); - const users = await getSpecificUsers(participants); - - return users.filter((x) => x?.type === type).map((x) => x?.id); -}; - async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "POST") return post(req, res); + if (req.method === "POST") return post(req, res); - return res.status(404).json({ok: false}); + return res.status(404).json({ ok: false }); } async function post(req: NextApiRequest, res: NextApiResponse) { - const maker = req.session.user; - if (!maker) { - return res.status(401).json({ok: false, reason: "You must be logged in to make user!"}); - } + const maker = req.session.user; + if (!maker) { + return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" }); + } - const corporateCorporate = await getUserCorporate(maker.id); + const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as { + email: string; + password?: string; + passport_id: string; + type: string; + entity: string; + groupID?: string; + corporate?: string; + expiryDate: null | Date; + }; - const {email, passport_id, password, type, groupID, expiryDate, corporate} = req.body as { - email: string; - password?: string; - passport_id: string; - type: string; - groupID?: string; - corporate?: string; - expiryDate: null | Date; - }; - // cleaning data - delete req.body.passport_id; - delete req.body.groupID; - delete req.body.expiryDate; - delete req.body.password; - delete req.body.corporate; + // cleaning data + delete req.body.passport_id; + delete req.body.groupID; + delete req.body.expiryDate; + delete req.body.password; + delete req.body.corporate; + delete req.body.entity - await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id) - .then(async (userCredentials) => { - const userId = userCredentials.user.uid; + await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id) + .then(async (userCredentials) => { + const userId = userCredentials.user.uid; - const profilePicture = !corporateCorporate ? "/defaultAvatar.png" : corporateCorporate.profilePicture; + const user = { + ...req.body, + bio: "", + id: userId, + type: type, + focus: "academic", + status: "active", + desiredLevels: DEFAULT_DESIRED_LEVELS, + profilePicture: "/defaultAvatar.png", + levels: DEFAULT_LEVELS, + isFirstLogin: false, + isVerified: true, + registrationDate: new Date(), + entities: [{ id: entity, role: "90ce8f08-08c8-41e4-9848-f1500ddc3930" }], + subscriptionExpirationDate: expiryDate || null, + ...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate" + ? { + corporateInformation: { + companyInformation: { + name: maker.corporateInformation?.companyInformation?.name || "N/A", + userAmount: 0, + }, + }, + } + : {}), + }; - const user = { - ...req.body, - bio: "", - id: userId, - type: type, - focus: "academic", - status: "active", - desiredLevels: DEFAULT_DESIRED_LEVELS, - profilePicture, - levels: DEFAULT_LEVELS, - isFirstLogin: false, - isVerified: true, - registrationDate: new Date(), - subscriptionExpirationDate: expiryDate || null, - ...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate" - ? { - corporateInformation: { - companyInformation: { - name: maker.corporateInformation?.companyInformation?.name || "N/A", - userAmount: 0, - }, - }, - } - : {}), - }; + const uid = new ShortUniqueId(); + const code = uid.randomUUID(6); - const uid = new ShortUniqueId(); - const code = uid.randomUUID(6); + await db.collection("users").insertOne(user); + await db.collection("codes").insertOne({ + code, + creator: maker.id, + expiryDate, + type, + creationDate: new Date(), + userId, + email: email.toLowerCase(), + name: req.body.name, + ...(!!passport_id ? { passport_id } : {}), + }); - await db.collection("users").insertOne(user); - await db.collection("codes").insertOne({ - code, - creator: maker.id, - expiryDate, - type, - creationDate: new Date(), - userId, - email: email.toLowerCase(), - name: req.body.name, - ...(!!passport_id ? {passport_id} : {}), - }); + if (!!groupID) { + const group = await getGroup(groupID); + if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } }); + } - if (type === "corporate") { - const students = maker.type === "corporate" ? await getUsersOfType(maker.id, "student") : []; - const teachers = maker.type === "corporate" ? await getUsersOfType(maker.id, "teacher") : []; + console.log(`Returning - ${email}`); + return res.status(200).json({ ok: true }); + }) + .catch((error) => { + if (error.code.includes("email-already-in-use")) return res.status(403).json({ error, message: "E-mail is already in the platform." }); - const defaultTeachersGroup: Group = { - admin: userId, - id: v4(), - name: "Teachers", - participants: teachers, - disableEditing: true, - }; - - const defaultStudentsGroup: Group = { - admin: userId, - id: v4(), - name: "Students", - participants: students, - disableEditing: true, - }; - - await db.collection("groups").insertMany([defaultStudentsGroup, defaultTeachersGroup]); - } - - if (!!corporate) { - const corporateUser = await db.collection("users").findOne({email: corporate.trim().toLowerCase()}); - - if (!!corporateUser) { - await db.collection("codes").updateOne({code}, {$set: {creator: corporateUser.id}}); - const typeGroup = await db - .collection("groups") - .findOne({creator: corporateUser.id, name: type === "student" ? "Students" : "Teachers"}); - - if (!!typeGroup) { - if (!typeGroup.participants.includes(userId)) { - await db.collection("groups").updateOne({id: typeGroup.id}, {$set: {participants: [...typeGroup.participants, userId]}}); - } - } else { - const defaultGroup: Group = { - admin: corporateUser.id, - id: v4(), - name: type === "student" ? "Students" : "Teachers", - participants: [userId], - disableEditing: true, - }; - - await db.collection("groups").insertOne(defaultGroup); - } - } - } - - if (maker.type === "corporate") { - await db.collection("codes").updateOne({code}, {$set: {creator: maker.id}}); - const typeGroup = await getUserNamedGroup(maker.id, type === "student" ? "Students" : "Teachers"); - - if (!!typeGroup) { - if (!typeGroup.participants.includes(userId)) { - await db.collection("groups").updateOne({id: typeGroup.id}, {$set: {participants: [...typeGroup.participants, userId]}}); - } - } else { - const defaultGroup: Group = { - admin: maker.id, - id: v4(), - name: type === "student" ? "Students" : "Teachers", - participants: [userId], - disableEditing: true, - }; - - await db.collection("groups").insertOne(defaultGroup); - } - } - - if (!!corporateCorporate && corporateCorporate.type === "mastercorporate" && type === "corporate") { - const corporateGroup = await getUserNamedGroup(corporateCorporate.id, "Corporate"); - - if (!!corporateGroup) { - if (!corporateGroup.participants.includes(userId)) { - await db - .collection("groups") - .updateOne({id: corporateGroup.id}, {$set: {participants: [...corporateGroup.participants, userId]}}); - } - } else { - const defaultGroup: Group = { - admin: corporateCorporate.id, - id: v4(), - name: "Corporate", - participants: [userId], - disableEditing: true, - }; - - await db.collection("groups").insertOne(defaultGroup); - } - } - - if (!!groupID) { - const group = await getGroup(groupID); - if (!!group) await db.collection("groups").updateOne({id: group.id}, {$set: {participants: [...group.participants, userId]}}); - } - - console.log(`Returning - ${email}`); - return res.status(200).json({ok: true}); - }) - .catch((error) => { - if (error.code.includes("email-already-in-use")) return res.status(403).json({error, message: "E-mail is already in the platform."}); - - console.log(`Failing - ${email}`); - console.log(error); - return res.status(401).json({error}); - }); + console.log(`Failing - ${email}`); + console.log(error); + return res.status(401).json({ error }); + }); } diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts index c1e8ceac..9ae196ca 100644 --- a/src/pages/api/user.ts +++ b/src/pages/api/user.ts @@ -7,7 +7,8 @@ import {withIronSessionApiRoute} from "iron-session/next"; import {NextApiRequest, NextApiResponse} from "next"; import {getPermissions, getPermissionDocs} from "@/utils/permissions.be"; import client from "@/lib/mongodb"; -import {getGroupsForUser, getParticipantGroups} from "@/utils/groups.be"; +import {getGroupsForUser, getParticipantGroups, removeParticipantFromGroup} from "@/utils/groups.be"; +import { mapBy } from "@/utils"; const auth = getAuth(adminApp); const db = client.db(process.env.MONGODB_DB); @@ -41,20 +42,6 @@ async function del(req: NextApiRequest, res: NextApiResponse) { return; } - if (user.type === "corporate" && (targetUser.type === "student" || targetUser.type === "teacher")) { - const groups = await getGroupsForUser(user.id, targetUser.id); - await Promise.all([ - ...groups - .filter((x) => x.admin === user.id) - .map( - async (x) => - await db.collection("groups").updateOne({id: x.id}, {$set: {participants: x.participants.filter((y: string) => y !== id)}}), - ), - ]); - - return; - } - await auth.deleteUser(id); await db.collection("users").deleteOne({id: targetUser.id}); await db.collection("codes").deleteMany({userId: targetUser.id}); @@ -62,11 +49,10 @@ async function del(req: NextApiRequest, res: NextApiResponse) { await db.collection("stats").deleteMany({user: targetUser.id}); const groups = await getParticipantGroups(targetUser.id); - await Promise.all( - groups.map( - async (x) => await db.collection("groups").updateOne({id: x.id}, {$set: {participants: x.participants.filter((y: string) => y !== id)}}), - ), - ); + await Promise.all( + groups + .map(async (g) => await removeParticipantFromGroup(g.id, targetUser.id)), + ); res.json({ok: true}); } diff --git a/src/pages/dashboard/admin.tsx b/src/pages/dashboard/admin.tsx new file mode 100644 index 00000000..52cc3d02 --- /dev/null +++ b/src/pages/dashboard/admin.tsx @@ -0,0 +1,195 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import UserDisplayList from "@/components/UserDisplayList"; +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 { useRouter } from "next/router"; +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 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 ( + <> + + EnCoach + + + + + + +
+ router.push("/list/users?type=student")} + Icon={BsPersonFill} + label="Students" + value={students.length} + color="purple" + /> + router.push("/list/users?type=teacher")} + Icon={BsPencilSquare} + label="Teachers" + value={teachers.length} + color="purple" + /> + router.push("/list/users?type=corporate")} + label="Corporates" + value={corporates.length} + color="purple" + /> + router.push("/list/users?type=mastercorporate")} + label="Master Corporates" + value={masterCorporates.length} + color="purple" + /> + + + + + + router.push("/assignments")} + label="Assignments" + value={assignments.filter((a) => !a.archived).length} + color="purple" + /> +
+ +
+ dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Students" + /> + dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Teachers" + /> + calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + title="Highest level students" + /> + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + } + title="Highest exam count students" + /> +
+
+ + ); +} diff --git a/src/pages/dashboard/admin/index.tsx b/src/pages/dashboard/admin/index.tsx deleted file mode 100644 index 9b50f662..00000000 --- a/src/pages/dashboard/admin/index.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/* 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 {useRouter} from "next/router"; -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 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); - }; - - const UserDisplay = (displayUser: User) => ( -
- {displayUser.name} -
- {displayUser.name} - {displayUser.email} -
-
- ); - - return ( - <> - - EnCoach - - - - - - -
- router.push("/lists/users?type=student")} - Icon={BsPersonFill} - label="Students" - value={students.length} - color="purple" - /> - router.push("/lists/users?type=teacher")} - Icon={BsPencilSquare} - label="Teachers" - value={teachers.length} - color="purple" - /> - router.push("/lists/users?type=corporate")} - label="Corporates" - value={corporates.length} - color="purple" - /> - router.push("/lists/users?type=mastercorporate")} - label="Master Corporates" - value={masterCorporates.length} - color="purple" - /> - - - - - - router.push("/assignments")} - label="Assignments" - value={assignments.filter((a) => !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/index.tsx b/src/pages/dashboard/corporate.tsx similarity index 77% rename from src/pages/dashboard/corporate/index.tsx rename to src/pages/dashboard/corporate.tsx index 4ce58192..1c7576d6 100644 --- a/src/pages/dashboard/corporate/index.tsx +++ b/src/pages/dashboard/corporate.tsx @@ -1,5 +1,6 @@ /* eslint-disable @next/next/no-img-element */ import Layout from "@/components/High/Layout"; +import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/dashboards/IconCard"; import { Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; @@ -136,14 +137,14 @@ export default function Dashboard({ user, users, entities, assignments, stats, g )}
router.push("/lists/users?type=student")} + onClick={() => router.push("/list/users?type=student")} Icon={BsPersonFill} label="Students" value={students.length} color="purple" /> router.push("/lists/users?type=teacher")} + onClick={() => router.push("/list/users?type=teacher")} Icon={BsPencilSquare} label="Teachers" value={teachers.length} @@ -178,50 +179,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
-
- 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 + dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Students" + /> + dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Teachers" + /> + calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + title="Highest level students" + /> + Object.keys(groupByExam(filterBy(stats, "user", b))).length - Object.keys(groupByExam(filterBy(stats, "user", a))).length, ) - .map((x) => ( - - ))} -
-
+ } + title="Highest exam count students" + />
diff --git a/src/pages/dashboard/developer.tsx b/src/pages/dashboard/developer.tsx new file mode 100644 index 00000000..47723ae2 --- /dev/null +++ b/src/pages/dashboard/developer.tsx @@ -0,0 +1,201 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import UserDisplayList from "@/components/UserDisplayList"; +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 { useRouter } from "next/router"; +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 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 ( + <> + + EnCoach + + + + + + +
+ router.push("/list/users?type=student")} + Icon={BsPersonFill} + label="Students" + value={students.length} + color="purple" + /> + router.push("/list/users?type=teacher")} + Icon={BsPencilSquare} + label="Teachers" + value={teachers.length} + color="purple" + /> + router.push("/list/users?type=corporate")} + label="Corporates" + value={corporates.length} + color="purple" + /> + router.push("/list/users?type=mastercorporate")} + label="Master Corporates" + value={masterCorporates.length} + color="purple" + /> + router.push("/entities")} + label="Classrooms" + value={groups.length} + color="purple" + /> + + + + + router.push("/assignments")} + label="Assignments" + value={assignments.filter((a) => !a.archived).length} + color="purple" + /> +
+ +
+ dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Students" + /> + dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Teachers" + /> + calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + title="Highest level students" + /> + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + } + title="Highest exam count students" + /> +
+
+ + ); +} diff --git a/src/pages/dashboard/developer/index.tsx b/src/pages/dashboard/developer/index.tsx deleted file mode 100644 index 9b50f662..00000000 --- a/src/pages/dashboard/developer/index.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/* 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 {useRouter} from "next/router"; -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 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); - }; - - const UserDisplay = (displayUser: User) => ( -
- {displayUser.name} -
- {displayUser.name} - {displayUser.email} -
-
- ); - - return ( - <> - - EnCoach - - - - - - -
- router.push("/lists/users?type=student")} - Icon={BsPersonFill} - label="Students" - value={students.length} - color="purple" - /> - router.push("/lists/users?type=teacher")} - Icon={BsPencilSquare} - label="Teachers" - value={teachers.length} - color="purple" - /> - router.push("/lists/users?type=corporate")} - label="Corporates" - value={corporates.length} - color="purple" - /> - router.push("/lists/users?type=mastercorporate")} - label="Master Corporates" - value={masterCorporates.length} - color="purple" - /> - - - - - - router.push("/assignments")} - label="Assignments" - value={assignments.filter((a) => !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/mastercorporate/index.tsx b/src/pages/dashboard/mastercorporate.tsx similarity index 74% rename from src/pages/dashboard/mastercorporate/index.tsx rename to src/pages/dashboard/mastercorporate.tsx index df32c467..01e58e2d 100644 --- a/src/pages/dashboard/mastercorporate/index.tsx +++ b/src/pages/dashboard/mastercorporate.tsx @@ -1,5 +1,6 @@ /* eslint-disable @next/next/no-img-element */ import Layout from "@/components/High/Layout"; +import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/dashboards/IconCard"; import { Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; @@ -133,21 +134,21 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
router.push("/lists/users?type=student")} + onClick={() => router.push("/list/users?type=student")} Icon={BsPersonFill} label="Students" value={students.length} color="purple" /> router.push("/lists/users?type=teacher")} + onClick={() => router.push("/list/users?type=teacher")} Icon={BsPencilSquare} label="Teachers" value={teachers.length} color="purple" /> router.push("/lists/users?type=corporate")} Icon={BsBank} label="Corporate Accounts" value={corporates.length} color="purple" /> + onClick={() => router.push("/list/users?type=corporate")} Icon={BsBank} label="Corporate Accounts" value={corporates.length} color="purple" /> router.push("/classrooms")} Icon={BsPeople} label="Classrooms" value={groups.length} color="purple" /> @@ -169,50 +170,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
-
- 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 + dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Students" + /> + dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Teachers" + /> + calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + title="Highest level students" + /> + Object.keys(groupByExam(filterBy(stats, "user", b))).length - Object.keys(groupByExam(filterBy(stats, "user", a))).length, ) - .map((x) => ( - - ))} -
-
+ } + title="Highest exam count students" + />
diff --git a/src/pages/dashboard/student/index.tsx b/src/pages/dashboard/student.tsx similarity index 100% rename from src/pages/dashboard/student/index.tsx rename to src/pages/dashboard/student.tsx diff --git a/src/pages/dashboard/teacher.tsx b/src/pages/dashboard/teacher.tsx new file mode 100644 index 00000000..32bd978c --- /dev/null +++ b/src/pages/dashboard/teacher.tsx @@ -0,0 +1,170 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import UserDisplayList from "@/components/UserDisplayList"; +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 Head from "next/head"; +import { useRouter } from "next/router"; +import { useMemo } from "react"; +import { BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill } 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 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); + }; + + const UserDisplay = (displayUser: User) => ( +
+ {displayUser.name} +
+ {displayUser.name} + {displayUser.email} +
+
+ ); + + return ( + <> + + EnCoach + + + + + + +
+ {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} +
+ + router.push("/classrooms")} + Icon={BsPeople} + label="Classrooms" + value={groups.length} + color="purple" + /> + + + router.push("/assignments")} + label="Assignments" + value={assignments.filter((a) => !a.archived).length} + color="purple" + /> +
+
+ +
+ dateSorter(a, b, "desc", "registrationDate"))} + title="Latest Students" + /> + calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} + title="Highest level students" + /> + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length, + ) + } + title="Highest exam count students" + /> +
+
+ + ); +} diff --git a/src/pages/dashboard/teacher/index.tsx b/src/pages/dashboard/teacher/index.tsx deleted file mode 100644 index dc163117..00000000 --- a/src/pages/dashboard/teacher/index.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/* 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 Head from "next/head"; -import {useRouter} from "next/router"; -import {useMemo} from "react"; -import {BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill} 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 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); - }; - - const UserDisplay = (displayUser: User) => ( -
- {displayUser.name} -
- {displayUser.name} - {displayUser.email} -
-
- ); - - return ( - <> - - EnCoach - - - - - - -
- {entities.length > 0 && ( -
- {mapBy(entities, "label")?.join(", ")} -
- )} -
- - router.push("/classrooms")} - Icon={BsPeople} - label="Classrooms" - value={groups.length} - color="purple" - /> - - - router.push("/assignments")} - label="Assignments" - value={assignments.filter((a) => !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/list/users.tsx b/src/pages/list/users.tsx index 26069756..06c6fdcd 100644 --- a/src/pages/list/users.tsx +++ b/src/pages/list/users.tsx @@ -1,26 +1,21 @@ import Layout from "@/components/High/Layout"; import useUser from "@/hooks/useUser"; import useUsers from "@/hooks/useUsers"; +import { Type, User } from "@/interfaces/user"; import {sessionOptions} from "@/lib/session"; import useFilterStore from "@/stores/listFilterStore"; +import { serialize } from "@/utils"; import {withIronSessionSsr} from "iron-session/next"; import Head from "next/head"; import {useRouter} from "next/router"; import {useEffect} from "react"; -import {BsArrowLeft} from "react-icons/bs"; +import {BsArrowLeft, BsChevronLeft} from "react-icons/bs"; import {ToastContainer} from "react-toastify"; import UserList from "../(admin)/Lists/UserList"; -export const getServerSideProps = withIronSessionSsr(({req, res}) => { +export const getServerSideProps = withIronSessionSsr(({req, res, query}) => { 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]!; - }); - if (!user) { return { redirect: { @@ -30,17 +25,22 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { }; } + const {type} = query as {type?: Type} + return { - props: {user: req.session.user, envVariables}, + props: serialize({user: req.session.user, type}), }; }, sessionOptions); -export default function UsersListPage() { - const {user} = useUser(); +interface Props { + user: User + type?: Type +} +export default function UsersListPage({ user, type }: Props) { const [filters, clearFilters] = useFilterStore((state) => [state.userFilters, state.clearUserFilters]); - const router = useRouter(); + const router = useRouter() return ( <> @@ -59,19 +59,19 @@ export default function UsersListPage() { f.filter)} renderHeader={(total) => ( -
-
+
-

Users ({total})

+ className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl"> + + +

Users ({ total })

)} /> diff --git a/src/pages/lists/users.tsx b/src/pages/lists/users.tsx deleted file mode 100644 index c74286aa..00000000 --- a/src/pages/lists/users.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import Layout from "@/components/High/Layout"; -import List from "@/components/List"; -import useUser from "@/hooks/useUser"; -import useUsers from "@/hooks/useUsers"; -import {Type, User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import useFilterStore from "@/stores/listFilterStore"; -import {mapBy, serialize} from "@/utils"; -import {getEntitiesUsers, getEntityUsers} from "@/utils/users.be"; -import {createColumnHelper} from "@tanstack/react-table"; -import clsx from "clsx"; -import {withIronSessionSsr} from "iron-session/next"; -import Head from "next/head"; -import {useRouter} from "next/router"; -import {useEffect, useState} from "react"; -import {BsArrowLeft, BsCheck, BsChevronLeft} from "react-icons/bs"; -import {ToastContainer} from "react-toastify"; -import UserList from "../(admin)/Lists/UserList"; -import {countries, TCountries} from "countries-list"; -import countryCodes from "country-codes-list"; -import {capitalize} from "lodash"; -import moment from "moment"; -import {USER_TYPE_LABELS} from "@/resources/user"; -import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; -import {EntityWithRoles} from "@/interfaces/entity"; - -const columnHelper = createColumnHelper(); - -const expirationDateColor = (date: Date) => { - const momentDate = moment(date); - const today = moment(new Date()); - - if (today.isAfter(momentDate)) return "!text-mti-red-light font-bold line-through"; - if (today.add(1, "weeks").isAfter(momentDate)) return "!text-mti-red-light"; - if (today.add(2, "weeks").isAfter(momentDate)) return "!text-mti-rose-light"; - if (today.add(1, "months").isAfter(momentDate)) return "!text-mti-orange-light"; -}; - -const SEARCH_FIELDS = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; - -export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => { - const user = req.session.user as User | undefined; - - if (!user) - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - - const {type} = query as {type?: Type}; - const users = await getEntitiesUsers(mapBy(user.entities, "id")); - const entities = await getEntitiesWithRoles(); - - const filters: ((u: User) => boolean)[] = []; - if (type) filters.push((u) => u.type === type); - - return { - props: serialize({user, users: filters.reduce((d, f) => d.filter(f), users), entities}), - }; -}, sessionOptions); - -interface Props { - user: User; - users: User[]; - entities: EntityWithRoles[]; -} - -export default function UsersList({user, users, entities}: Props) { - const [showDemographicInformation, setShowDemographicInformation] = useState(false); - const router = useRouter(); - - const defaultColumns = [ - columnHelper.accessor("name", { - header: ( - - ) as any, - cell: ({row, getValue}) =>
{getValue()}
, - }), - columnHelper.accessor("email", { - header: ( - - ) as any, - cell: ({row, getValue}) =>
{getValue()}
, - }), - columnHelper.accessor("type", { - header: ( - - ) as any, - cell: (info) => USER_TYPE_LABELS[info.getValue()], - }), - columnHelper.accessor("studentID", { - header: ( - - ) as any, - cell: (info) => info.getValue() || "N/A", - }), - columnHelper.accessor("entities", { - header: ( - - ) as any, - cell: ({getValue}) => - getValue() - .map((e) => entities.find((x) => x.id === e.id)?.label) - .join(", "), - }), - columnHelper.accessor("subscriptionExpirationDate", { - header: ( - - ) as any, - cell: (info) => ( - - {!info.getValue() ? "No expiry date" : moment(info.getValue()).format("DD/MM/YYYY")} - - ), - }), - columnHelper.accessor("isVerified", { - header: ( - - ) as any, - cell: (info) => ( -
-
- -
-
- ), - }), - { - header: ( - setShowDemographicInformation((prev) => !prev)}> - Switch - - ), - id: "actions", - cell: () => "", - }, - ]; - - const demographicColumns = [ - columnHelper.accessor("name", { - header: ( - - ) as any, - cell: ({row, getValue}) =>
{getValue()}
, - }), - columnHelper.accessor("demographicInformation.country", { - header: ( - - ) as any, - cell: (info) => - info.getValue() - ? `${countryCodes.findOne("countryCode" as any, info.getValue())?.flag} ${ - countries[info.getValue() as unknown as keyof TCountries]?.name - } (+${countryCodes.findOne("countryCode" as any, info.getValue())?.countryCallingCode})` - : "N/A", - }), - columnHelper.accessor("demographicInformation.phone", { - header: ( - - ) as any, - cell: (info) => info.getValue() || "N/A", - enableSorting: true, - }), - columnHelper.accessor( - (x) => - x.type === "corporate" || x.type === "mastercorporate" ? x.demographicInformation?.position : x.demographicInformation?.employment, - { - id: "employment", - header: ( - - ) as any, - cell: (info) => (info.row.original.type === "corporate" ? info.getValue() : capitalize(info.getValue())) || "N/A", - enableSorting: true, - }, - ), - columnHelper.accessor("lastLogin", { - header: ( - - ) as any, - cell: (info) => (!!info.getValue() ? moment(info.getValue()).format("YYYY-MM-DD HH:mm") : "N/A"), - }), - columnHelper.accessor("demographicInformation.gender", { - header: ( - - ) as any, - cell: (info) => capitalize(info.getValue()) || "N/A", - enableSorting: true, - }), - { - header: ( - setShowDemographicInformation((prev) => !prev)}> - Switch - - ), - id: "actions", - cell: () => "", - }, - ]; - - return ( - <> - - EnCoach - - - - - - - -
- -

User List

-
- data={users} searchFields={SEARCH_FIELDS} columns={showDemographicInformation ? demographicColumns : defaultColumns} /> -
- - ); -} diff --git a/src/pages/record.tsx b/src/pages/record.tsx index c4d520d0..72bc156b 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -3,7 +3,7 @@ import Head from "next/head"; import {withIronSessionSsr} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {Stat, User} from "@/interfaces/user"; -import {useEffect, useRef, useState} from "react"; +import {useEffect, useMemo, useRef, useState} from "react"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; import {groupByDate} from "@/utils/stats"; import moment from "moment"; @@ -22,9 +22,18 @@ import RecordFilter from "@/components/Medium/RecordFilter"; import {useRouter} from "next/router"; import useTrainingContentStore from "@/stores/trainingContentStore"; import {Assignment} from "@/interfaces/results"; -import {getUsers} from "@/utils/users.be"; -import {getAssignments, getAssignmentsByAssigner} from "@/utils/assignments.be"; +import {getEntitiesUsers, getUsers} from "@/utils/users.be"; +import {getAssignments, getAssignmentsByAssigner, getEntitiesAssignments} from "@/utils/assignments.be"; import useGradingSystem from "@/hooks/useGrading"; +import { mapBy, serialize } from "@/utils"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; +import { checkAccess } from "@/utils/permissions"; +import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; +import { getGradingSystemByEntity } from "@/utils/grading.be"; +import { Grading } from "@/interfaces"; +import { EntityWithRoles } from "@/interfaces/entity"; +import { useListSearch } from "@/hooks/useListSearch"; +import CardList from "@/components/High/CardList"; export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { const user = req.session.user; @@ -47,11 +56,16 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { }; } - const users = await getUsers(); - const assignments = await getAssignments(); + const entityIDs = mapBy(user.entities, 'id') + + const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs) + const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id'))) + const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id'))) + const assignments = await (checkAccess(user, ["admin", "developer"]) ? getAssignments() : getEntitiesAssignments(mapBy(entities, 'id'))) + const gradingSystems = await Promise.all(entityIDs.map(getGradingSystemByEntity)) return { - props: {user, users, assignments}, + props: serialize({user, users, assignments, entities, gradingSystems}), }; }, sessionOptions); @@ -61,9 +75,13 @@ interface Props { user: User; users: User[]; assignments: Assignment[]; + gradingSystems: Grading[] + entities: EntityWithRoles[] } -export default function History({user, users, assignments}: Props) { +const MAX_TRAINING_EXAMS = 10; + +export default function History({user, users, assignments, entities, gradingSystems}: Props) { const router = useRouter(); const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [ state.selectedUser, @@ -72,8 +90,6 @@ export default function History({user, users, assignments}: Props) { state.setTraining, ]); - // const [statsUserId, setStatsUserId] = useState(user.id); - const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>(); const [filter, setFilter] = useState(); const {data: stats, isLoading: isStatsLoading} = useFilterRecordsByUser(statsUserId || user?.id); @@ -87,30 +103,32 @@ export default function History({user, users, assignments}: Props) { const setTimeSpent = useExamStore((state) => state.setTimeSpent); const renderPdfIcon = usePDFDownload("stats"); + const [selectedTrainingExams, setSelectedTrainingExams] = useState([]); + const setTrainingStats = useTrainingContentStore((state) => state.setStats); + + const groupedStats = useMemo(() => groupByDate( + stats.filter((x) => { + if ( + (x.module === "writing" || x.module === "speaking") && + !x.isDisabled && + !x.solutions.every((y) => Object.keys(y).includes("evaluation")) + ) + return false; + return true; + }), + ), [stats]) + useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]); useEffect(() => { - if (stats && !isStatsLoading) { - setGroupedStats( - groupByDate( - stats.filter((x) => { - if ( - (x.module === "writing" || x.module === "speaking") && - !x.isDisabled && - !x.solutions.every((y) => Object.keys(y).includes("evaluation")) - ) - return false; - return true; - }), - ), - ); - } - }, [stats, isStatsLoading]); - - // useEffect(() => { - // // just set this initially - // if (!statsUserId) setStatsUserId(user.id); - // }, []); + const handleRouteChange = (url: string) => { + setTraining(false); + }; + router.events.on("routeChangeStart", handleRouteChange); + return () => { + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [router.events, setTraining]); const filterStatsByDate = (stats: {[key: string]: Stat[]}) => { if (filter && filter !== "assignments") { @@ -139,39 +157,29 @@ export default function History({user, users, assignments}: Props) { return stats; }; - const MAX_TRAINING_EXAMS = 10; - const [selectedTrainingExams, setSelectedTrainingExams] = useState([]); - const setTrainingStats = useTrainingContentStore((state) => state.setStats); +const handleTrainingContentSubmission = () => { + if (groupedStats) { + const groupedStatsByDate = filterStatsByDate(groupedStats); + const allStats = Object.keys(groupedStatsByDate); + const selectedStats = selectedTrainingExams.reduce>((accumulator, moduleAndTimestamp) => { + const timestamp = moduleAndTimestamp.split("-")[1]; + if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) { + accumulator[timestamp] = groupedStatsByDate[timestamp]; + } + return accumulator; + }, {}); + setTrainingStats(Object.values(selectedStats).flat()); + router.push("/training"); + } +}; - const handleTrainingContentSubmission = () => { - if (groupedStats) { - const groupedStatsByDate = filterStatsByDate(groupedStats); - const allStats = Object.keys(groupedStatsByDate); - const selectedStats = selectedTrainingExams.reduce>((accumulator, moduleAndTimestamp) => { - const timestamp = moduleAndTimestamp.split("-")[1]; - if (allStats.includes(timestamp) && !accumulator.hasOwnProperty(timestamp)) { - accumulator[timestamp] = groupedStatsByDate[timestamp]; - } - return accumulator; - }, {}); - setTrainingStats(Object.values(selectedStats).flat()); - router.push("/training"); - } - }; - - useEffect(() => { - const handleRouteChange = (url: string) => { - setTraining(false); - }; - router.events.on("routeChangeStart", handleRouteChange); - return () => { - router.events.off("routeChangeStart", handleRouteChange); - }; - }, [router.events, setTraining]); + const filteredStats = useMemo(() => + Object.keys(filterStatsByDate(groupedStats)) + .sort((a, b) => parseInt(b) - parseInt(a)), + // eslint-disable-next-line react-hooks/exhaustive-deps + [groupedStats, filter]) const customContent = (timestamp: string) => { - if (!groupedStats) return <>; - const dateStats = groupedStats[timestamp]; return ( @@ -212,7 +220,7 @@ export default function History({user, users, assignments}: Props) { {user && ( - + {training && (
@@ -231,14 +239,12 @@ export default function History({user, users, assignments}: Props) {
)} - {groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && ( -
- {Object.keys(filterStatsByDate(groupedStats)) - .sort((a, b) => parseInt(b) - parseInt(a)) - .map(customContent)} -
+ + + {filteredStats.length > 0 && !isStatsLoading && ( + )} - {groupedStats && Object.keys(groupedStats).length === 0 && !isStatsLoading && ( + {filteredStats.length === 0 && !isStatsLoading && ( No record to display... )} {isStatsLoading && ( diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index e3dce976..26bcdb14 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -1,156 +1,161 @@ /* 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 { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { ToastContainer } from "react-toastify"; import Layout from "@/components/High/Layout"; import CodeGenerator from "./(admin)/CodeGenerator"; import ExamLoader from "./(admin)/ExamLoader"; -import {Tab} from "@headlessui/react"; +import { Tab } from "@headlessui/react"; import clsx from "clsx"; import Lists from "./(admin)/Lists"; import BatchCodeGenerator from "./(admin)/BatchCodeGenerator"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; import ExamGenerator from "./(admin)/ExamGenerator"; import BatchCreateUser from "./(admin)/BatchCreateUser"; -import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import { checkAccess, getTypesOfUser } from "@/utils/permissions"; import usePermissions from "@/hooks/usePermissions"; -import {useState} from "react"; +import { useState } from "react"; import Modal from "@/components/Modal"; import IconCard from "@/dashboards/IconCard"; -import {BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill} from "react-icons/bs"; +import { BsCode, BsCodeSquare, BsGearFill, BsPeopleFill, BsPersonFill } from "react-icons/bs"; import UserCreator from "./(admin)/UserCreator"; import CorporateGradingSystem from "./(admin)/CorporateGradingSystem"; import useGradingSystem from "@/hooks/useGrading"; -import {CEFR_STEPS} from "@/resources/grading"; -import {User} from "@/interfaces/user"; -import {getUserPermissions} from "@/utils/permissions.be"; -import {Permission, PermissionType} from "@/interfaces/permissions"; -import {getUsers} from "@/utils/users.be"; +import { CEFR_STEPS } from "@/resources/grading"; +import { User } from "@/interfaces/user"; +import { getUserPermissions } from "@/utils/permissions.be"; +import { Permission, PermissionType } from "@/interfaces/permissions"; +import { getUsers } from "@/utils/users.be"; import useUsers from "@/hooks/useUsers"; +import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be"; +import { mapBy, serialize } from "@/utils"; +import { EntityWithRoles } from "@/interfaces/entity"; -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { - const user = req.session.user; - if (!user) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } +export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { + const user = req.session.user as User; + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"])) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"])) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - const permissions = await getUserPermissions(user.id); + const permissions = await getUserPermissions(user.id); + const entities = await getEntitiesWithRoles(mapBy(user.entities, 'id')) || [] - return { - props: {user, permissions}, - }; + return { + props: serialize({ user, permissions, entities }), + }; }, sessionOptions); interface Props { - user: User; - permissions: PermissionType[]; + user: User; + permissions: PermissionType[]; + entities: EntityWithRoles[] } -export default function Admin({user, permissions}: Props) { - const {gradingSystem, mutate} = useGradingSystem(); - const {users} = useUsers(); +export default function Admin({ user, entities, permissions }: Props) { + const { gradingSystem, mutate } = useGradingSystem(); + const { users } = useUsers(); - const [modalOpen, setModalOpen] = useState(); + const [modalOpen, setModalOpen] = useState(); - return ( - <> - - Settings Panel | EnCoach - - - - - - - setModalOpen(undefined)}> - setModalOpen(undefined)} /> - - setModalOpen(undefined)}> - setModalOpen(undefined)} /> - - setModalOpen(undefined)}> - setModalOpen(undefined)} /> - - setModalOpen(undefined)}> - setModalOpen(undefined)} /> - - setModalOpen(undefined)}> - { - mutate({user: user.id, steps}); - setModalOpen(undefined); - }} - /> - + return ( + <> + + Settings Panel | EnCoach + + + + + + + setModalOpen(undefined)}> + setModalOpen(undefined)} /> + + setModalOpen(undefined)}> + setModalOpen(undefined)} /> + + setModalOpen(undefined)}> + setModalOpen(undefined)} /> + + setModalOpen(undefined)}> + setModalOpen(undefined)} /> + + setModalOpen(undefined)}> + { + mutate({ user: user.id, steps }); + setModalOpen(undefined); + }} + /> + -
- - {checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && ( -
- setModalOpen("createCode")} - /> - setModalOpen("batchCreateCode")} - /> - setModalOpen("createUser")} - /> - setModalOpen("batchCreateUser")} - /> - {checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && ( - setModalOpen("gradingSystem")} - /> - )} -
- )} -
-
- -
-
- - ); +
+ + {checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && ( +
+ setModalOpen("createCode")} + /> + setModalOpen("batchCreateCode")} + /> + setModalOpen("createUser")} + /> + setModalOpen("batchCreateUser")} + /> + {checkAccess(user, ["admin", "corporate", "developer", "mastercorporate"]) && ( + setModalOpen("gradingSystem")} + /> + )} +
+ )} +
+
+ +
+
+ + ); } diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index 4c45090f..a4f2e3d6 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -17,22 +17,28 @@ import {calculateAverageLevel, calculateBandScore} from "@/utils/score"; import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils"; import {Chart} from "react-chartjs-2"; import useUsers from "@/hooks/useUsers"; -import Select from "react-select"; import useGroups from "@/hooks/useGroups"; import DatePicker from "react-datepicker"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import ProfileSummary from "@/components/ProfileSummary"; import moment from "moment"; -import {Stat} from "@/interfaces/user"; +import {Group, Stat, User} from "@/interfaces/user"; import {Divider} from "primereact/divider"; import Badge from "@/components/Low/Badge"; +import { mapBy, serialize } from "@/utils"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; +import { checkAccess } from "@/utils/permissions"; +import { getEntitiesUsers, getUsers } from "@/utils/users.be"; +import { EntityWithRoles } from "@/interfaces/entity"; +import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; +import Select from "@/components/Low/Select"; ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip); const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"]; -export const getServerSideProps = withIronSessionSsr(({req, res}) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User; if (!user) { return { @@ -52,13 +58,25 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { }; } + const entityIDs = mapBy(user.entities, 'id') + const entities = await getEntitiesWithRoles(checkAccess(user, ["admin", "developer"]) ? undefined : entityIDs) + const users = await (checkAccess(user, ["admin", "developer"]) ? getUsers() : getEntitiesUsers(mapBy(entities, 'id'))) + const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id'))) + return { - props: {user: req.session.user}, + props: serialize({user, entities, users, groups}), }; }, sessionOptions); -export default function Stats() { - const [statsUserId, setStatsUserId] = useState(); +interface Props { + user: User + users: User[] + entities: EntityWithRoles[] + groups: Group[] +} + +export default function Stats({ user, entities, users, groups }: Props) { + const [statsUserId, setStatsUserId] = useState(user.id); const [startDate, setStartDate] = useState(moment(new Date()).subtract(1, "weeks").toDate()); const [endDate, setEndDate] = useState(new Date()); const [initialStatDate, setInitialStatDate] = useState(); @@ -69,15 +87,8 @@ export default function Stats() { const [dailyScoreDate, setDailyScoreDate] = useState(new Date()); const [intervalDates, setIntervalDates] = useState([]); - const {user} = useUser({redirectTo: "/login"}); - const {users} = useUsers(); - const {groups} = useGroups({admin: user?.id}); const {data: stats} = useFilterRecordsByUser(statsUserId, !statsUserId); - useEffect(() => { - if (user) setStatsUserId(user.id); - }, [user]); - useEffect(() => { setInitialStatDate( stats @@ -190,16 +201,7 @@ export default function Stats() { className="w-full" options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))} defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} - onChange={(value) => setStatsUserId(value?.value)} - menuPortalTarget={document?.body} - styles={{ - menuPortal: (base) => ({...base, zIndex: 9999}), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} + onChange={(value) => setStatsUserId(value?.value || user.id)} /> )} {["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && ( @@ -209,16 +211,7 @@ export default function Stats() { .filter((x) => groups.flatMap((y) => y.participants).includes(x.id)) .map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))} defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}} - onChange={(value) => setStatsUserId(value?.value)} - menuPortalTarget={document?.body} - styles={{ - menuPortal: (base) => ({...base, zIndex: 9999}), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} + onChange={(value) => setStatsUserId(value?.value || user.id)} /> )} diff --git a/src/pages/training/index.tsx b/src/pages/training/index.tsx index 461ca135..be1af30b 100644 --- a/src/pages/training/index.tsx +++ b/src/pages/training/index.tsx @@ -20,9 +20,15 @@ import TrainingScore from "@/training/TrainingScore"; import ModuleBadge from "@/components/ModuleBadge"; import RecordFilter from "@/components/Medium/RecordFilter"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; +import { mapBy, serialize } from "@/utils"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; +import { getAssignmentsByAssignee } from "@/utils/assignments.be"; +import { getEntitiesUsers } from "@/utils/users.be"; +import { EntityWithRoles } from "@/interfaces/entity"; +import { Assignment } from "@/interfaces/results"; -export const getServerSideProps = withIronSessionSsr(({req, res}) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user as User; if (!user) { return { @@ -42,12 +48,16 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { }; } + const entityIDs = mapBy(user.entities, 'id') + const entities = await getEntitiesWithRoles(entityIDs) + const users = await getEntitiesUsers(entityIDs) + return { - props: {user: req.session.user}, + props: serialize({user, users, entities}), }; }, sessionOptions); -const Training: React.FC<{user: User}> = ({user}) => { +const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[] }> = ({user, entities, users}) => { const [recordUserId, setRecordTraining] = useRecordStore((state) => [state.selectedUser, state.setTraining]); const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">(); @@ -193,7 +203,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
) : ( <> - + {user.type === "student" && ( <>
diff --git a/src/resources/speakingAvatars.ts b/src/resources/speakingAvatars.ts index ddff86ac..b15d5821 100644 --- a/src/resources/speakingAvatars.ts +++ b/src/resources/speakingAvatars.ts @@ -1,37 +1,37 @@ export const AVATARS = [ { - name: "Matthew Noah", - id: "5912afa7c77c47d3883af3d874047aaf", - gender: "male", - }, - { - name: "Vera Cerise", - id: "9e58d96a383e4568a7f1e49df549e0e4", + name: "Gia", + id: "gia.business", gender: "female", }, { - name: "Edward Tony", - id: "d2cdd9c0379a4d06ae2afb6e5039bd0c", + name: "Vadim", + id: "vadim.business", gender: "male", }, { - name: "Tanya Molly", - id: "045cb5dcd00042b3a1e4f3bc1c12176b", - gender: "female", - }, - { - name: "Kayla Abbi", - id: "1ae1e5396cc444bfad332155fdb7a934", - gender: "female", - }, - { - name: "Jerome Ryan", - id: "0ee6aa7cc1084063a630ae514fccaa31", + name: "Orhan", + id: "orhan.business", gender: "male", }, { - name: "Tyler Christopher", - id: "5772cff935844516ad7eeff21f839e43", + name: "Flora", + id: "flora.business", + gender: "female", + }, + { + name: "Scarlett", + id: "scarlett.business", + gender: "female", + }, + { + name: "Parker", + id: "parker.casual", + gender: "male", + }, + { + name: "Ethan", + id: "ethan.business", gender: "male", }, ]; diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 24996911..1981f519 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -1,132 +1,171 @@ -import {app} from "@/firebase"; -import {Assignment} from "@/interfaces/results"; -import {CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User} from "@/interfaces/user"; +import { app } from "@/firebase"; +import { WithEntity } from "@/interfaces/entity"; +import { Assignment } from "@/interfaces/results"; +import { CorporateUser, Group, GroupWithUsers, MasterCorporateUser, StudentUser, TeacherUser, Type, User } from "@/interfaces/user"; import client from "@/lib/mongodb"; import moment from "moment"; -import {getLinkedUsers, getUser} from "./users.be"; -import {getSpecificUsers} from "./users.be"; +import { getLinkedUsers, getUser } from "./users.be"; +import { getSpecificUsers } from "./users.be"; const db = client.db(process.env.MONGODB_DB); +const addEntityToGroupPipeline = [ + { + $lookup: { + from: "entities", + localField: "entity", + foreignField: "id", + as: "entity" + } + }, + { + $addFields: { + entity: { $arrayElemAt: ["$entity", 0] } + } + }, + { + $addFields: { + entity: { + $cond: { + if: { $isArray: "$entity" }, + then: undefined, + else: "$entity" + } + } + } + } +] + export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => { - const corporate = await db.collection("users").findOne({id: corporateID}); - const participant = await db.collection("users").findOne({id: participantID}); + const corporate = await db.collection("users").findOne({ id: corporateID }); + const participant = await db.collection("users").findOne({ id: participantID }); - if (!corporate || !participant) return; - if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return; + if (!corporate || !participant) return; + if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return; - if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) - return await db.collection("users").updateOne({id: participant.id}, {$set: {subscriptionExpirationDate: null}}); + if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) + return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: null } }); - const corporateDate = moment(corporate.subscriptionExpirationDate); - const participantDate = moment(participant.subscriptionExpirationDate); + const corporateDate = moment(corporate.subscriptionExpirationDate); + const participantDate = moment(participant.subscriptionExpirationDate); - if (corporateDate.isAfter(participantDate)) - return await db.collection("users").updateOne({id: participant.id}, {$set: {subscriptionExpirationDate: corporateDate.toISOString()}}); + if (corporateDate.isAfter(participantDate)) + return await db.collection("users").updateOne({ id: participant.id }, { $set: { subscriptionExpirationDate: corporateDate.toISOString() } }); - return; + return; }; export const getUserCorporate = async (id: string) => { - const user = await getUser(id); - if (!user) return undefined; + const user = await getUser(id); + if (!user) return undefined; - if (["admin", "developer"].includes(user.type)) return undefined; - if (user.type === "mastercorporate") return user; + if (["admin", "developer"].includes(user.type)) return undefined; + if (user.type === "mastercorporate") return user; - const groups = await getParticipantGroups(id); - const admins = await Promise.all(groups.map((x) => x.admin).map(getUser)); - const corporates = admins - .filter((x) => (user.type === "corporate" ? x?.type === "mastercorporate" : x?.type === "corporate")) - .filter((x) => !!x) as User[]; + const groups = await getParticipantGroups(id); + const admins = await Promise.all(groups.map((x) => x.admin).map(getUser)); + const corporates = admins + .filter((x) => (user.type === "corporate" ? x?.type === "mastercorporate" : x?.type === "corporate")) + .filter((x) => !!x) as User[]; - if (corporates.length === 0) return undefined; - return corporates.shift() as CorporateUser | MasterCorporateUser; + if (corporates.length === 0) return undefined; + return corporates.shift() as CorporateUser | MasterCorporateUser; }; export const getGroup = async (id: string) => { - return await db.collection("groups").findOne({id}); + return await db.collection("groups").findOne({ id }); }; -export const getGroups = async () => { - return await db.collection("groups").find({}).toArray(); +export const getGroups = async (): Promise[]> => { + return await db.collection("groups") + .aggregate>(addEntityToGroupPipeline).toArray() }; export const getParticipantGroups = async (id: string) => { - return await db.collection("groups").find({participants: id}).toArray(); + return await db.collection("groups").find({ participants: id }).toArray(); }; export const getUserGroups = async (id: string): Promise => { - return await db.collection("groups").find({admin: id}).toArray(); + return await db.collection("groups").find({ admin: id }).toArray(); }; export const getUserNamedGroup = async (id: string, name: string) => { - return await db.collection("groups").findOne({admin: id, name}); + return await db.collection("groups").findOne({ admin: id, name }); }; +export const removeParticipantFromGroup = async (id: string, user: string) => { + return await db.collection("groups").updateOne({id}, { + // @ts-expect-error + $pull: { + participants: user + } + }) +} + export const getUsersGroups = async (ids: string[]) => { - return await db - .collection("groups") - .find({admin: {$in: ids}}) - .toArray(); + return await db + .collection("groups") + .find({ admin: { $in: ids } }) + .toArray(); }; export const convertToUsers = (group: Group, users: User[]): GroupWithUsers => - Object.assign(group, { - admin: users.find((u) => u.id === group.admin), - participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[], - }); + Object.assign(group, { + admin: users.find((u) => u.id === group.admin), + participants: group.participants.map((p) => users.find((u) => u.id === p)).filter((x) => !!x) as User[], + }); export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise => { - const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher"); - const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate"); + const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher"); + const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate"); - return [...linkedTeachers.users.map((x) => x.id), ...linkedCorporates.users.map((x) => x.id)]; + return [...linkedTeachers.users.map((x) => x.id), ...linkedCorporates.users.map((x) => x.id)]; }; export const getGroupsForEntities = async (ids: string[]) => - await db - .collection("groups") - .find({entity: {$in: ids}}) - .toArray(); + await db + .collection("groups") + .find({ entity: { $in: ids } }) + .toArray(); export const getGroupsForUser = async (admin?: string, participant?: string) => { - if (admin && participant) return await db.collection("groups").find({admin, participant}).toArray(); + if (admin && participant) return await db.collection("groups").find({ admin, participant }).toArray(); - if (admin) return await getUserGroups(admin); - if (participant) return await getParticipantGroups(participant); + if (admin) return await getUserGroups(admin); + if (participant) return await getParticipantGroups(participant); - return await getGroups(); + return await getGroups(); }; export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => { - return await db - .collection("groups") - .find({...(admin ? {admin: {$ne: admin}} : {}), ...(participants ? {participants} : {})}) - .toArray(); + return await db + .collection("groups") + .find({ ...(admin ? { admin: { $ne: admin } } : {}), ...(participants ? { participants } : {}) }) + .toArray(); }; export const getCorporateNameForStudent = async (studentID: string) => { - const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]); - if (groups.length === 0) return ""; + const groups = await getStudentGroupsForUsersWithoutAdmin("", [studentID]); + if (groups.length === 0) return ""; - const adminUserIds = [...new Set(groups.map((g) => g.admin))]; - const adminUsersData = (await getSpecificUsers(adminUserIds)).filter((x) => !!x) as User[]; + const adminUserIds = [...new Set(groups.map((g) => g.admin))]; + const adminUsersData = (await getSpecificUsers(adminUserIds)).filter((x) => !!x) as User[]; - if (adminUsersData.length === 0) return ""; - const admins = adminUsersData.filter((x) => x.type === "corporate"); + if (adminUsersData.length === 0) return ""; + const admins = adminUsersData.filter((x) => x.type === "corporate"); - if (admins.length > 0) { - return (admins[0] as CorporateUser).corporateInformation.companyInformation.name; - } + if (admins.length > 0) { + return (admins[0] as CorporateUser).corporateInformation.companyInformation.name; + } - return ""; + return ""; }; -export const getGroupsByEntity = async (id: string) => await db.collection("groups").find({entity: id}).toArray(); +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(); +export const getGroupsByEntities = async (ids: string[]): Promise[]> => + await db.collection("groups") + .aggregate>([ + { $match: { entity: { $in: ids } } }, + ...addEntityToGroupPipeline + ]).toArray() diff --git a/src/utils/index.ts b/src/utils/index.ts index 20ee1045..50adc521 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -44,7 +44,7 @@ export const convertBase64 = (file: File) => { }); }; -export const mapBy = (obj: T[] | undefined, key: keyof T) => (obj || []).map((i) => i[key]); +export const mapBy = (obj: T[] | undefined, key: K) => (obj || []).map((i) => i[key] as T[K]); 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/users.be.ts b/src/utils/users.be.ts index 963eef79..a6b180e9 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -1,140 +1,140 @@ -import {CorporateUser, Type, User} from "@/interfaces/user"; -import {getGroupsForUser, getParticipantGroups, getUserGroups, getUsersGroups} from "./groups.be"; -import {uniq} from "lodash"; -import {getUserCodes} from "./codes.be"; +import { CorporateUser, Type, User } from "@/interfaces/user"; +import { getGroupsForUser, getParticipantGroups, getUserGroups, getUsersGroups } from "./groups.be"; +import { uniq } from "lodash"; +import { getUserCodes } from "./codes.be"; import client from "@/lib/mongodb"; -import {WithEntity} from "@/interfaces/entity"; -import {getEntity} from "./entities.be"; -import {getRole} from "./roles.be"; +import { WithEntities } from "@/interfaces/entity"; +import { getEntity } from "./entities.be"; +import { getRole } from "./roles.be"; const db = client.db(process.env.MONGODB_DB); -export async function getUsers() { - return await db - .collection("users") - .find({}, {projection: {_id: 0}}) - .toArray(); +export async function getUsers(filter?: object) { + return await db + .collection("users") + .find(filter || {}, { projection: { _id: 0 } }) + .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; +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); + 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 { entity, role }; + }), + ); - return {...user, entities}; + 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; + const user = await db.collection("users").findOne({ id: id }, { projection: { _id: 0 } }); + return !!user ? user : undefined; } export async function getSpecificUsers(ids: string[]) { - if (ids.length === 0) return []; + if (ids.length === 0) return []; - return await db - .collection("users") - .find({id: {$in: ids}}, {projection: {_id: 0}}) - .toArray(); + return await db + .collection("users") + .find({ id: { $in: ids } }, { projection: { _id: 0 } }) + .toArray(); } export async function getEntityUsers(id: string, limit?: number) { - return await db - .collection("users") - .find({"entities.id": id}) - .limit(limit || 0) - .toArray(); + 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}); + 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 getEntitiesUsers(ids: string[], filter?: object, limit?: number) { + return await db + .collection("users") + .find({ "entities.id": { $in: ids }, ...(filter || {}) }) + .limit(limit || 0) + .toArray(); } export async function countEntitiesUsers(ids: string[]) { - return await db.collection("users").countDocuments({"entities.id": {$in: ids}}); + return await db.collection("users").countDocuments({ "entities.id": { $in: ids } }); } export async function getLinkedUsers( - userID?: string, - userType?: Type, - type?: Type, - page?: number, - size?: number, - sort?: string, - direction?: "asc" | "desc", + userID?: string, + userType?: Type, + type?: Type, + page?: number, + size?: number, + sort?: string, + direction?: "asc" | "desc", ) { - const filters = { - ...(!!type ? {type} : {}), - }; + const filters = { + ...(!!type ? { type } : {}), + }; - if (!userID || userType === "admin" || userType === "developer") { - const users = await db - .collection("users") - .find(filters) - .sort(sort ? {[sort]: direction === "desc" ? -1 : 1} : {}) - .skip(page && size ? page * size : 0) - .limit(size || 0) - .toArray(); + if (!userID || userType === "admin" || userType === "developer") { + const users = await db + .collection("users") + .find(filters) + .sort(sort ? { [sort]: direction === "desc" ? -1 : 1 } : {}) + .skip(page && size ? page * size : 0) + .limit(size || 0) + .toArray(); - const total = await db.collection("users").countDocuments(filters); - return {users, total}; - } + const total = await db.collection("users").countDocuments(filters); + return { users, total }; + } - const adminGroups = await getUserGroups(userID); - const groups = await getUsersGroups(adminGroups.flatMap((x) => x.participants)); - const belongingGroups = await getParticipantGroups(userID); + const adminGroups = await getUserGroups(userID); + const groups = await getUsersGroups(adminGroups.flatMap((x) => x.participants)); + const belongingGroups = await getParticipantGroups(userID); - const participants = uniq([ - ...adminGroups.flatMap((x) => x.participants), - ...(userType === "mastercorporate" ? groups.flat().flatMap((x) => x.participants) : []), - ...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []), - ]); + const participants = uniq([ + ...adminGroups.flatMap((x) => x.participants), + ...(userType === "mastercorporate" ? groups.flat().flatMap((x) => x.participants) : []), + ...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []), + ]); - // ⨯ [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] { - if (participants.length === 0) return {users: [], total: 0}; + // ⨯ [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] { + if (participants.length === 0) return { users: [], total: 0 }; - const users = await db - .collection("users") - .find({...filters, id: {$in: participants}}) - .skip(page && size ? page * size : 0) - .limit(size || 0) - .toArray(); - const total = await db.collection("users").countDocuments({...filters, id: {$in: participants}}); + const users = await db + .collection("users") + .find({ ...filters, id: { $in: participants } }) + .skip(page && size ? page * size : 0) + .limit(size || 0) + .toArray(); + const total = await db.collection("users").countDocuments({ ...filters, id: { $in: participants } }); - return {users, total}; + return { users, total }; } export async function getUserBalance(user: User) { - const codes = await getUserCodes(user.id); - if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length; + const codes = await getUserCodes(user.id); + if (user.type !== "corporate" && user.type !== "mastercorporate") return codes.length; - const groups = await getGroupsForUser(user.id); - const participants = uniq(groups.flatMap((x) => x.participants)); + const groups = await getGroupsForUser(user.id); + const participants = uniq(groups.flatMap((x) => x.participants)); - if (user.type === "corporate") return participants.length + codes.filter((x) => !participants.includes(x.userId || "")).length; + if (user.type === "corporate") return participants.length + codes.filter((x) => !participants.includes(x.userId || "")).length; - const participantUsers = await Promise.all(participants.map(getUser)); - const corporateUsers = participantUsers.filter((x) => x?.type === "corporate") as CorporateUser[]; + const participantUsers = await Promise.all(participants.map(getUser)); + const corporateUsers = participantUsers.filter((x) => x?.type === "corporate") as CorporateUser[]; - return ( - corporateUsers.reduce((acc, curr) => acc + curr.corporateInformation?.companyInformation?.userAmount || 0, 0) + - corporateUsers.length + - codes.filter((x) => !participants.includes(x.userId || "") && !corporateUsers.map((u) => u.id).includes(x.userId || "")).length - ); + return ( + corporateUsers.reduce((acc, curr) => acc + curr.corporateInformation?.companyInformation?.userAmount || 0, 0) + + corporateUsers.length + + codes.filter((x) => !participants.includes(x.userId || "") && !corporateUsers.map((u) => u.id).includes(x.userId || "")).length + ); } diff --git a/src/utils/users.ts b/src/utils/users.ts index bdcb262e..89b1353b 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,45 +1,46 @@ -import {Group, User} from "@/interfaces/user"; -import {getUserCompanyName, USER_TYPE_LABELS} from "@/resources/user"; -import {capitalize} from "lodash"; +import { WithLabeledEntities } from "@/interfaces/entity"; +import { Group, User } from "@/interfaces/user"; +import { getUserCompanyName, USER_TYPE_LABELS } from "@/resources/user"; +import { capitalize } from "lodash"; import moment from "moment"; export interface UserListRow { - name: string; - email: string; - type: string; - companyName: string; - expiryDate: string; - verified: string; - country: string; - phone: string; - employmentPosition: string; - gender: string; + name: string; + email: string; + type: string; + entities: string; + expiryDate: string; + verified: string; + country: string; + phone: string; + employmentPosition: string; + gender: string; } -export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group[]) => { - const rows: UserListRow[] = rowUsers.map((user) => ({ - name: user.name, - email: user.email, - type: USER_TYPE_LABELS[user.type], - companyName: getUserCompanyName(user, users, groups), - expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited", - country: user.demographicInformation?.country || "N/A", - phone: user.demographicInformation?.phone || "N/A", - employmentPosition: - (user.type === "corporate" || user.type === "mastercorporate" - ? user.demographicInformation?.position - : user.demographicInformation?.employment) || "N/A", - gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A", - verified: user.isVerified?.toString() || "FALSE", - })); - const header = "Name,Email,Type,Company Name,Expiry Date,Country,Phone,Employment/Department,Gender,Verification"; - const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n"); +export const exportListToExcel = (rowUsers: WithLabeledEntities[]) => { + const rows: UserListRow[] = rowUsers.map((user) => ({ + name: user.name, + email: user.email, + type: USER_TYPE_LABELS[user.type], + entities: user.entities.map((e) => e.label).join(', '), + expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited", + country: user.demographicInformation?.country || "N/A", + phone: user.demographicInformation?.phone || "N/A", + employmentPosition: + (user.type === "corporate" || user.type === "mastercorporate" + ? user.demographicInformation?.position + : user.demographicInformation?.employment) || "N/A", + gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A", + verified: user.isVerified?.toString() || "FALSE", + })); + const header = "Name,Email,Type,Entities,Expiry Date,Country,Phone,Employment/Department,Gender,Verification"; + const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n"); - return `${header}\n${rowsString}`; + return `${header}\n${rowsString}`; }; export const getUserName = (user?: User) => { - if (!user) return "N/A"; - if (user.type === "corporate" || user.type === "mastercorporate") return user.corporateInformation?.companyInformation?.name || user.name; - return user.name; + if (!user) return "N/A"; + if (user.type === "corporate" || user.type === "mastercorporate") return user.corporateInformation?.companyInformation?.name || user.name; + return user.name; };