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") && (
)}
- {checkAccess(user, ["developer", "admin", "teacher", "student"], permissions) && (
+ {checkAccess(user, ["developer", "admin", "mastercorporate", "corporate", "teacher", "student"], permissions) && (
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
diff --git a/src/email/templates/resetPassword.handlebars b/src/email/templates/resetPassword.handlebars
new file mode 100644
index 00000000..487c3a1a
--- /dev/null
+++ b/src/email/templates/resetPassword.handlebars
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ Hi {{name}},
+ You requested to reset your password.
+ Please, click the link below to reset your password
+ Reset Password
+
+
+
\ No newline at end of file
diff --git a/src/hooks/useListSearch.tsx b/src/hooks/useListSearch.tsx
index 512a644a..af4ed1e4 100644
--- a/src/hooks/useListSearch.tsx
+++ b/src/hooks/useListSearch.tsx
@@ -5,7 +5,7 @@ import {search} from "@/utils/search";
export function useListSearch(fields: string[][], rows: T[]) {
const [text, setText] = useState("");
- const renderSearch = () => ;
+ const renderSearch = () => ;
const updatedRows = useMemo(() => {
if (text.length > 0) return search(text, fields, rows);
diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx
index 490c6c9e..9075dbbe 100644
--- a/src/hooks/usePagination.tsx
+++ b/src/hooks/usePagination.tsx
@@ -1,5 +1,7 @@
import Button from "@/components/Low/Button";
import {useMemo, useState} from "react";
+import {BiChevronLeft} from "react-icons/bi";
+import {BsChevronDoubleLeft, BsChevronDoubleRight, BsChevronLeft, BsChevronRight} from "react-icons/bs";
export default function usePagination(list: T[], size = 25) {
const [page, setPage] = useState(0);
@@ -24,5 +26,35 @@ export default function usePagination(list: T[], size = 25) {
);
- 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};
}