From dd94228672a814f9c8e16b28029d37f310183831 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 25 Sep 2024 16:18:43 +0100 Subject: [PATCH] Created a new system for the Groups that will persist after having entities --- src/components/Low/Checkbox.tsx | 2 +- src/components/Low/Separator.tsx | 3 + src/components/Low/Tooltip.tsx | 17 + src/components/Sidebar.tsx | 4 +- src/email/templates/resetPassword.handlebars | 16 + src/hooks/useListSearch.tsx | 2 +- src/hooks/usePagination.tsx | 34 +- src/interfaces/entity.ts | 16 + src/interfaces/user.ts | 10 +- src/pages/api/groups/[id].ts | 13 +- src/pages/api/groups/index.ts | 15 +- src/pages/groups.tsx | 114 ------- src/pages/groups/[id].tsx | 336 +++++++++++++++++++ src/pages/groups/create.tsx | 204 +++++++++++ src/pages/groups/index.tsx | 137 ++++++++ src/utils/groups.be.ts | 8 +- src/utils/search.ts | 18 +- src/utils/users.be.ts | 10 +- 18 files changed, 823 insertions(+), 136 deletions(-) create mode 100644 src/components/Low/Separator.tsx create mode 100644 src/components/Low/Tooltip.tsx create mode 100644 src/email/templates/resetPassword.handlebars create mode 100644 src/interfaces/entity.ts delete mode 100644 src/pages/groups.tsx create mode 100644 src/pages/groups/[id].tsx create mode 100644 src/pages/groups/create.tsx create mode 100644 src/pages/groups/index.tsx diff --git a/src/components/Low/Checkbox.tsx b/src/components/Low/Checkbox.tsx index fa90951f..d527bc91 100644 --- a/src/components/Low/Checkbox.tsx +++ b/src/components/Low/Checkbox.tsx @@ -5,7 +5,7 @@ import {BsCheck} from "react-icons/bs"; interface Props { isChecked: boolean; onChange: (isChecked: boolean) => void; - children: ReactNode; + children?: ReactNode; disabled?: boolean; } diff --git a/src/components/Low/Separator.tsx b/src/components/Low/Separator.tsx new file mode 100644 index 00000000..5772b705 --- /dev/null +++ b/src/components/Low/Separator.tsx @@ -0,0 +1,3 @@ +const Separator = () =>
; + +export default Separator; diff --git a/src/components/Low/Tooltip.tsx b/src/components/Low/Tooltip.tsx new file mode 100644 index 00000000..bbe26a1f --- /dev/null +++ b/src/components/Low/Tooltip.tsx @@ -0,0 +1,17 @@ +import clsx from "clsx"; +import {ReactNode} from "react"; + +interface Props { + tooltip: string; + disabled?: boolean; + className?: string; + children: ReactNode; +} + +export default function Tooltip({tooltip, disabled = false, className, children}: Props) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 19d5d40f..85f806bc 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -57,7 +57,7 @@ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, "flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white", "transition-all duration-300 ease-in-out relative", disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer", - path === keyPath && "bg-mti-purple-light text-white", + (keyPath === "/" ? path === keyPath : path.startsWith(keyPath)) && "bg-mti-purple-light text-white", isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]", )}> @@ -110,7 +110,7 @@ export default function Sidebar({path, navDisabled = false, focusMode = false, u {checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
); - return {page, items, setPage, render}; + const renderMinimal = () => ( +
+
+ + +
+ + {page * size + 1} - {(page + 1) * size > list.length ? list.length : (page + 1) * size} / {list.length} + +
+ + +
+
+ ); + + return {page, items, setPage, render, renderMinimal}; } diff --git a/src/interfaces/entity.ts b/src/interfaces/entity.ts new file mode 100644 index 00000000..def83ddb --- /dev/null +++ b/src/interfaces/entity.ts @@ -0,0 +1,16 @@ +export interface Entity { + id: string; + label: string; +} + +export interface Roles { + id: string; + permissions: string[]; + label: string; +} + +export interface EntityWithPermissions extends Entity { + roles: Roles[]; +} + +export type WithEntity = T extends {entities: string[]} ? T & {entities: Entity[]} : T; diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 110650a5..4609a17e 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -22,6 +22,7 @@ export interface BasicUser { status: UserStatus; permissions: PermissionType[]; lastLogin?: Date; + entities: string[]; } export interface StudentUser extends BasicUser { @@ -151,6 +152,11 @@ export interface Group { disableEditing?: boolean; } +export interface GroupWithUsers extends Omit { + admin: User; + participants: User[]; +} + export interface Code { id: string; code: string; @@ -165,4 +171,6 @@ export interface Code { } export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate"; -export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; \ No newline at end of file +export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; + +export type WithUser = T extends {participants: string[]} ? Omit & {participants: User[]} : T; diff --git a/src/pages/api/groups/[id].ts b/src/pages/api/groups/[id].ts index 48df6782..9290c479 100644 --- a/src/pages/api/groups/[id].ts +++ b/src/pages/api/groups/[id].ts @@ -75,13 +75,20 @@ async function patch(req: NextApiRequest, res: NextApiResponse) { } const user = req.session.user; - if (user.type === "admin" || user.type === "developer" || user.id === group.admin) { - if ("participants" in req.body) { + if ( + user.type === "admin" || + user.type === "developer" || + user.type === "mastercorporate" || + user.type === "corporate" || + user.id === group.admin + ) { + if ("participants" in req.body && req.body.participants.length > 0) { const newParticipants = (req.body.participants as string[]).filter((x) => !group.participants.includes(x)); await Promise.all(newParticipants.map(async (p) => await updateExpiryDateOnGroup(p, group.admin))); } - await db.collection("groups").updateOne({id: req.session.user.id}, {$set: {id, ...req.body}}, {upsert: true}); + console.log(req.body); + await db.collection("groups").updateOne({id}, {$set: {id, ...req.body}}, {upsert: true}); res.status(200).json({ok: true}); return; diff --git a/src/pages/api/groups/index.ts b/src/pages/api/groups/index.ts index 1165c522..854db5fe 100644 --- a/src/pages/api/groups/index.ts +++ b/src/pages/api/groups/index.ts @@ -39,11 +39,12 @@ async function post(req: NextApiRequest, res: NextApiResponse) { await Promise.all(body.participants.map(async (p) => await updateExpiryDateOnGroup(p, body.admin))); - await db.collection("groups").insertOne({ - id: v4(), - name: body.name, - admin: body.admin, - participants: body.participants, - }) - res.status(200).json({ok: true}); + const id = v4(); + await db.collection("groups").insertOne({ + id, + name: body.name, + admin: body.admin, + participants: body.participants, + }); + res.status(200).json({ok: true, id}); } diff --git a/src/pages/groups.tsx b/src/pages/groups.tsx deleted file mode 100644 index 12886c91..00000000 --- a/src/pages/groups.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -import Head from "next/head"; -import Navbar from "@/components/Navbar"; -import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMegaphone} from "react-icons/bs"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {useEffect, useState} from "react"; -import {averageScore, groupBySession, totalExams} from "@/utils/stats"; -import useUser from "@/hooks/useUser"; -import Diagnostic from "@/components/Diagnostic"; -import {ToastContainer} from "react-toastify"; -import {capitalize} from "lodash"; -import {Module} from "@/interfaces"; -import ProgressBar from "@/components/Low/ProgressBar"; -import Layout from "@/components/High/Layout"; -import {calculateAverageLevel} from "@/utils/score"; -import axios from "axios"; -import DemographicInformationInput from "@/components/DemographicInformationInput"; -import moment from "moment"; -import Link from "next/link"; -import {MODULE_ARRAY} from "@/utils/moduleUtils"; -import ProfileSummary from "@/components/ProfileSummary"; -import StudentDashboard from "@/dashboards/Student"; -import AdminDashboard from "@/dashboards/Admin"; -import CorporateDashboard from "@/dashboards/Corporate"; -import TeacherDashboard from "@/dashboards/Teacher"; -import AgentDashboard from "@/dashboards/Agent"; -import MasterCorporateDashboard from "@/dashboards/MasterCorporate"; -import PaymentDue from "./(status)/PaymentDue"; -import {useRouter} from "next/router"; -import {PayPalScriptProvider} from "@paypal/react-paypal-js"; -import {CorporateUser, Group, MasterCorporateUser, Type, User, userTypes} from "@/interfaces/user"; -import Select from "react-select"; -import {USER_TYPE_LABELS} from "@/resources/user"; -import {checkAccess, getTypesOfUser} from "@/utils/permissions"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import useGroups from "@/hooks/useGroups"; -import useUsers from "@/hooks/useUsers"; -import {getUserName} from "@/utils/users"; -import {getParticipantGroups, getUserGroups} from "@/utils/groups.be"; -import {getUsers} from "@/utils/users.be"; - -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { - const user = req.session.user; - - if (!user) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } - - if (shouldRedirectHome(user)) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - const groups = await getParticipantGroups(user.id); - const users = await getUsers(); - - return { - props: {user, groups, users}, - }; -}, sessionOptions); - -interface Props { - user: User; - groups: Group[]; - users: User[]; -} -export default function Home({user, groups, users}: Props) { - return ( - <> - - EnCoach - - - - - - {user && ( - -
- {groups - .filter((x) => x.participants.includes(user.id)) - .map((group) => ( -
- - Group: - {group.name} - - - Admin: - {getUserName(users.find((x) => x.id === group.admin))} - - Participants: - {group.participants.map((x) => getUserName(users.find((u) => u.id === x))).join(", ")} -
- ))} -
-
- )} - - ); -} diff --git a/src/pages/groups/[id].tsx b/src/pages/groups/[id].tsx new file mode 100644 index 00000000..f208de00 --- /dev/null +++ b/src/pages/groups/[id].tsx @@ -0,0 +1,336 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import Checkbox from "@/components/Low/Checkbox"; +import Tooltip from "@/components/Low/Tooltip"; +import {useListSearch} from "@/hooks/useListSearch"; +import usePagination from "@/hooks/usePagination"; +import {GroupWithUsers, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {USER_TYPE_LABELS} from "@/resources/user"; +import {convertToUsers, getGroup} from "@/utils/groups.be"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import {getUserName} from "@/utils/users"; +import {getLinkedUsers, getSpecificUsers, getUsers} from "@/utils/users.be"; +import axios from "axios"; +import clsx from "clsx"; +import {withIronSessionSsr} from "iron-session/next"; +import moment from "moment"; +import Head from "next/head"; +import Link from "next/link"; +import {useRouter} from "next/router"; +import {Divider} from "primereact/divider"; +import {useEffect, useMemo, useState} from "react"; +import { + BsArrowLeft, + BsChevronLeft, + BsClockFill, + BsEnvelopeFill, + BsFillPersonVcardFill, + BsPencil, + BsPerson, + BsPlus, + BsStopwatchFill, + BsTag, + BsTrash, + BsX, +} from "react-icons/bs"; +import {toast, ToastContainer} from "react-toastify"; + +export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { + const user = req.session.user as User; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const {id} = params as {id: string}; + + const group = await getGroup(id); + if (!group || (checkAccess(user, getTypesOfUser(["admin", "developer"])) && group.admin !== user.id && !group.participants.includes(user.id))) { + return { + redirect: { + destination: "/groups", + permanent: false, + }, + }; + } + + const linkedUsers = await getLinkedUsers(user.id, user.type); + const users = await getSpecificUsers([...group.participants, group.admin]); + const groupWithUser = convertToUsers(group, users); + + return { + props: {user, group: JSON.parse(JSON.stringify(groupWithUser)), users: JSON.parse(JSON.stringify(linkedUsers.users))}, + }; +}, sessionOptions); + +interface Props { + user: User; + group: GroupWithUsers; + users: User[]; +} + +export default function Home({user, group, users}: Props) { + const [isAdding, setIsAdding] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + + const nonParticipantUsers = useMemo( + () => users.filter((x) => ![...group.participants.map((g) => g.id), group.admin.id, user.id].includes(x.id)), + [users, group.participants, group.admin], + ); + + const {rows, renderSearch} = useListSearch( + [["name"], ["corporateInformation", "companyInformation", "name"]], + isAdding ? nonParticipantUsers : group.participants, + ); + const {items, renderMinimal} = usePagination(rows, 20); + + const router = useRouter(); + + const allowGroupEdit = useMemo(() => checkAccess(user, ["admin", "developer", "mastercorporate"]) || user.id === group.admin.id, [user, group]); + + const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id])); + + const removeParticipants = () => { + if (selectedUsers.length === 0) return; + if (!allowGroupEdit) return; + if (!confirm(`Are you sure you want to remove ${selectedUsers.length} participant${selectedUsers.length === 1 ? "" : "s"} from this group?`)) + return; + + setIsLoading(true); + + axios + .patch(`/api/groups/${group.id}`, {participants: group.participants.map((x) => x.id).filter((x) => !selectedUsers.includes(x))}) + .then(() => { + toast.success("The group has been updated successfully!"); + setTimeout(() => router.reload(), 500); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const addParticipants = () => { + if (selectedUsers.length === 0) return; + if (!allowGroupEdit || !isAdding) return; + if (!confirm(`Are you sure you want to add ${selectedUsers.length} participant${selectedUsers.length === 1 ? "" : "s"} to this group?`)) + return; + + setIsLoading(true); + + console.log([...group.participants.map((x) => x.id), selectedUsers]); + axios + .patch(`/api/groups/${group.id}`, {participants: [...group.participants.map((x) => x.id), ...selectedUsers]}) + .then(() => { + toast.success("The group has been updated successfully!"); + setTimeout(() => router.reload(), 500); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const renameGroup = () => { + if (!allowGroupEdit) return; + + const name = prompt("Rename this group:", group.name); + if (!name) return; + + setIsLoading(true); + axios + .patch(`/api/groups/${group.id}`, {name}) + .then(() => { + toast.success("The group has been updated successfully!"); + setTimeout(() => router.reload(), 500); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const deleteGroup = () => { + if (!allowGroupEdit) return; + if (!confirm("Are you sure you want to delete this group?")) return; + + setIsLoading(true); + + axios + .delete(`/api/groups/${group.id}`) + .then(() => { + toast.success("This group has been successfully deleted!"); + setTimeout(() => router.push("/groups"), 1000); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + useEffect(() => setSelectedUsers([]), [isAdding]); + + return ( + <> + + {group.name} | EnCoach + + + + + + {user && ( + +
+
+
+
+ + + +

{group.name}

+
+ + {getUserName(group.admin)} + +
+ {allowGroupEdit && !isAdding && ( +
+ + +
+ )} +
+ +
+ Participants + {allowGroupEdit && !isAdding && ( +
+ + +
+ )} + {allowGroupEdit && isAdding && ( +
+ + +
+ )} +
+
+ {renderSearch()} + {renderMinimal()} +
+
+ +
+ {items.map((u) => ( + + ))} +
+
+ )} + + ); +} diff --git a/src/pages/groups/create.tsx b/src/pages/groups/create.tsx new file mode 100644 index 00000000..6935b9a8 --- /dev/null +++ b/src/pages/groups/create.tsx @@ -0,0 +1,204 @@ +/* eslint-disable @next/next/no-img-element */ +import Layout from "@/components/High/Layout"; +import Button from "@/components/Low/Button"; +import Checkbox from "@/components/Low/Checkbox"; +import Input from "@/components/Low/Input"; +import Tooltip from "@/components/Low/Tooltip"; +import {useListSearch} from "@/hooks/useListSearch"; +import usePagination from "@/hooks/usePagination"; +import {GroupWithUsers, User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {USER_TYPE_LABELS} from "@/resources/user"; +import {convertToUsers, getGroup} from "@/utils/groups.be"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import {getUserName} from "@/utils/users"; +import {getLinkedUsers, getSpecificUsers, getUsers} from "@/utils/users.be"; +import axios from "axios"; +import clsx from "clsx"; +import {withIronSessionSsr} from "iron-session/next"; +import moment from "moment"; +import Head from "next/head"; +import Link from "next/link"; +import {useRouter} from "next/router"; +import {Divider} from "primereact/divider"; +import {useEffect, useMemo, useState} from "react"; +import { + BsArrowLeft, + BsCheck, + BsChevronLeft, + BsClockFill, + BsEnvelopeFill, + BsFillPersonVcardFill, + BsPencil, + BsPerson, + BsPlus, + BsStopwatchFill, + BsTag, + BsTrash, + BsX, +} from "react-icons/bs"; +import {toast, ToastContainer} from "react-toastify"; + +export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { + const user = req.session.user as User; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const linkedUsers = await getLinkedUsers(user.id, user.type); + + return { + props: {user, users: JSON.parse(JSON.stringify(linkedUsers.users.filter((x) => x.id !== user.id)))}, + }; +}, sessionOptions); + +interface Props { + user: User; + users: User[]; +} + +export default function Home({user, users}: Props) { + const [isLoading, setIsLoading] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + const [name, setName] = useState(""); + + const {rows, renderSearch} = useListSearch([["name"], ["corporateInformation", "companyInformation", "name"]], users); + const {items, renderMinimal} = usePagination(rows, 16); + + const router = useRouter(); + + const createGroup = () => { + if (!name.trim()) return; + if (!confirm(`Are you sure you want to create this group with ${selectedUsers.length} participants?`)) return; + + setIsLoading(true); + + axios + .post<{id: string}>(`/api/groups`, {name, participants: selectedUsers, admin: user.id}) + .then((result) => { + toast.success("Your group has been created successfully!"); + setTimeout(() => router.push(`/groups/${result.data.id}`), 250); + }) + .catch((e) => { + console.error(e); + toast.error("Something went wrong!"); + }) + .finally(() => setIsLoading(false)); + }; + + const toggleUser = (u: User) => setSelectedUsers((prev) => (prev.includes(u.id) ? prev.filter((p) => p !== u.id) : [...prev, u.id])); + + return ( + <> + + Create Group | EnCoach + + + + + + {user && ( + +
+
+
+ + + +

Create Group

+
+
+ +
+
+ +
+ Group Name: + +
+ +
+ Participants ({selectedUsers.length} selected): +
+
+ {renderSearch()} + {renderMinimal()} +
+
+ +
+ {items.map((u) => ( + + ))} +
+
+ )} + + ); +} diff --git a/src/pages/groups/index.tsx b/src/pages/groups/index.tsx new file mode 100644 index 00000000..917583a5 --- /dev/null +++ b/src/pages/groups/index.tsx @@ -0,0 +1,137 @@ +/* eslint-disable @next/next/no-img-element */ +import Head from "next/head"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; +import {ToastContainer} from "react-toastify"; +import Layout from "@/components/High/Layout"; +import {Group, GroupWithUsers, User, WithUser} from "@/interfaces/user"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {getUserName} from "@/utils/users"; +import {convertToUsers, getGroupsForUser, getParticipantGroups, getUserGroups} from "@/utils/groups.be"; +import {getSpecificUsers, getUsers} from "@/utils/users.be"; +import {checkAccess} from "@/utils/permissions"; +import usePagination from "@/hooks/usePagination"; +import {useListSearch} from "@/hooks/useListSearch"; +import Link from "next/link"; +import {uniq} from "lodash"; +import {BsPlus} from "react-icons/bs"; +import {Divider} from "primereact/divider"; +import Separator from "@/components/Low/Separator"; + +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = req.session.user; + + if (!user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + if (shouldRedirectHome(user)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const groups = await getGroupsForUser( + checkAccess(user, ["corporate", "mastercorporate"]) ? user.id : undefined, + checkAccess(user, ["teacher", "student"]) ? user.id : undefined, + ); + + const users = await getSpecificUsers(uniq(groups.flatMap((g) => [...g.participants.slice(0, 5), g.admin]))); + const groupsWithUsers: GroupWithUsers[] = groups.map((g) => convertToUsers(g, users)); + + return { + props: {user, groups: JSON.parse(JSON.stringify(groupsWithUsers))}, + }; +}, sessionOptions); + +interface Props { + user: User; + groups: GroupWithUsers[]; +} +export default function Home({user, groups}: Props) { + const {rows, renderSearch} = useListSearch( + [ + ["name"], + ["admin", "name"], + ["admin", "email"], + ["admin", "corporateInformation", "companyInformation", "name"], + ["participants", "name"], + ["participants", "email"], + ["participants", "corporateInformation", "companyInformation", "name"], + ], + groups, + ); + const {items, page, renderMinimal} = usePagination(rows, 20); + + return ( + <> + + Groups | EnCoach + + + + + + {user && ( + +
+

Groups

+ +
+ +
+
+ {renderSearch()} + {renderMinimal()} +
+
+ {page === 0 && ( + + + Create Group + + )} + {items.map((group) => ( + + + Group: + {group.name} + + + Admin: + {getUserName(group.admin)} + + Participants ({group.participants.length}): + + {group.participants.slice(0, 5).map(getUserName).join(", ")} + {group.participants.length > 5 ? ( + and {group.participants.length - 5} more + ) : ( + "" + )} + + + ))} +
+
+
+ )} + + ); +} diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 4c344dc8..f500311b 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -1,6 +1,6 @@ import {app} from "@/firebase"; import {Assignment} from "@/interfaces/results"; -import {CorporateUser, Group, MasterCorporateUser, StudentUser, TeacherUser, Type, User} from "@/interfaces/user"; +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"; @@ -71,6 +71,12 @@ export const getUsersGroups = async (ids: string[]) => { .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[], + }); + export const getAllAssignersByCorporate = async (corporateID: string, type: Type): Promise => { const linkedTeachers = await getLinkedUsers(corporateID, type, "teacher"); const linkedCorporates = await getLinkedUsers(corporateID, type, "corporate"); diff --git a/src/utils/search.ts b/src/utils/search.ts index 87b2cfc1..d40e4246 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -3,11 +3,20 @@ ['companyInformation', 'companyInformation', 'name'] ]*/ -const getFieldValue = (fields: string[], data: any): string => { +const getFieldValue = (fields: string[], data: any): string | string[] => { if (fields.length === 0) return data; const [key, ...otherFields] = fields; - if (data[key]) return getFieldValue(otherFields, data[key]); + if (Array.isArray(data[key])) { + // If the key points to an array, like "participants", iterate through each item in the array + return data[key] + .map((item: any) => getFieldValue(otherFields, item)) // Get the value for each item + .filter(Boolean); // Filter out undefined or null values + } else if (data[key] !== undefined) { + // If it's not an array, just go deeper in the object + return getFieldValue(otherFields, data[key]); + } + return data; }; @@ -16,6 +25,11 @@ export const search = (text: string, fields: string[][], rows: any[]) => { return rows.filter((row) => { return fields.some((fieldsKeys) => { const value = getFieldValue(fieldsKeys, row); + if (Array.isArray(value)) { + // If it's an array (e.g., participants' names), check each value in the array + return value.some((v) => v && typeof v === "string" && v.toLowerCase().includes(searchText)); + } + if (typeof value === "string") { return value.toLowerCase().includes(searchText); } diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index 31ecbe05..caee2762 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -8,11 +8,14 @@ import client from "@/lib/mongodb"; const db = client.db(process.env.MONGODB_DB); export async function getUsers() { - return await db.collection("users").find({}, { projection: { _id: 0 } }).toArray(); + return await db + .collection("users") + .find({}, {projection: {_id: 0}}) + .toArray(); } export async function getUser(id: string): Promise { - const user = await db.collection("users").findOne({id: id}, { projection: { _id: 0 } }); + const user = await db.collection("users").findOne({id: id}, {projection: {_id: 0}}); return !!user ? user : undefined; } @@ -21,7 +24,7 @@ export async function getSpecificUsers(ids: string[]) { return await db .collection("users") - .find({id: {$in: ids}}, { projection: { _id: 0 } }) + .find({id: {$in: ids}}, {projection: {_id: 0}}) .toArray(); } @@ -46,6 +49,7 @@ export async function getLinkedUsers( .skip(page && size ? page * size : 0) .limit(size || 0) .toArray(); + const total = await db.collection("users").countDocuments(filters); return {users, total}; }