From 202632ff58dca06318a46c984bbbab1d53353616 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 22 Aug 2024 12:27:15 +0100 Subject: [PATCH 1/9] ENCOA-89, ENCOA-91, ENCOA-92, ENCOA-95 All changes related to permissions towards types of users --- src/components/UserCard.tsx | 6 +- src/constants/userPermissions.ts | 164 +++++++++++------------ src/pages/(admin)/BatchCodeGenerator.tsx | 6 +- src/pages/(admin)/BatchCreateUser.tsx | 40 ++++-- src/pages/(admin)/CodeGenerator.tsx | 10 +- src/pages/settings.tsx | 2 +- 6 files changed, 126 insertions(+), 102 deletions(-) diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index ba02b079..6705677d 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -248,7 +248,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, onChange={setCompanyName} placeholder="Enter corporate name" defaultValue={companyName} - disabled={disabled} + disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))} /> setUserAmount(e ? parseInt(e) : undefined)} placeholder="Enter number of users" defaultValue={userAmount} - disabled={disabled} + disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))} /> setMonthlyDuration(e ? parseInt(e) : undefined)} placeholder="Enter monthly duration" defaultValue={monthlyDuration} - disabled={disabled} + disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))} />
diff --git a/src/constants/userPermissions.ts b/src/constants/userPermissions.ts index af3e7543..8716c301 100644 --- a/src/constants/userPermissions.ts +++ b/src/constants/userPermissions.ts @@ -1,91 +1,91 @@ -import { Type } from "@/interfaces/user"; +import {Type} from "@/interfaces/user"; export const PERMISSIONS = { - generateCode: { - student: ["corporate", "developer", "admin", "mastercorporate"], - teacher: ["corporate", "developer", "admin", "mastercorporate"], - corporate: ["admin", "developer"], - mastercorporate: ["admin", "developer"], + generateCode: { + student: ["corporate", "developer", "admin", "mastercorporate"], + teacher: ["corporate", "developer", "admin", "mastercorporate"], + corporate: ["admin", "developer"], + mastercorporate: ["admin", "developer"], - admin: ["developer", "admin"], - agent: ["developer", "admin"], - developer: ["developer"], - }, - deleteUser: { - student: { - perm: "deleteStudent", - list: ["corporate", "developer", "admin", "mastercorporate"], - }, - teacher: { - perm: "deleteTeacher", - list: ["corporate", "developer", "admin", "mastercorporate"], - }, - corporate: { - perm: "deleteCorporate", - list: ["admin", "developer"], - }, - mastercorporate: { - perm: undefined, - list: ["admin", "developer"], - }, + admin: ["developer", "admin"], + agent: ["developer", "admin"], + developer: ["developer"], + }, + deleteUser: { + student: { + perm: "deleteStudent", + list: ["corporate", "developer", "admin", "mastercorporate"], + }, + teacher: { + perm: "deleteTeacher", + list: ["corporate", "developer", "admin", "mastercorporate"], + }, + corporate: { + perm: "deleteCorporate", + list: ["admin", "developer"], + }, + mastercorporate: { + perm: undefined, + list: ["admin", "developer"], + }, - admin: { - perm: "deleteAdmin", - list: ["developer", "admin"], - }, - agent: { - perm: "deleteCountryManager", - list: ["developer", "admin"], - }, - developer: { - perm: undefined, - list: ["developer"], - }, - }, - updateUser: { - student: { - perm: "editStudent", - list: ["developer", "admin"], - }, - teacher: { - perm: "editTeacher", - list: ["developer", "admin"], - }, + admin: { + perm: "deleteAdmin", + list: ["developer", "admin"], + }, + agent: { + perm: "deleteCountryManager", + list: ["developer", "admin"], + }, + developer: { + perm: undefined, + list: ["developer"], + }, + }, + updateUser: { + student: { + perm: "editStudent", + list: ["developer", "admin", "corporate", "mastercorporate", "teacher"], + }, + teacher: { + perm: "editTeacher", + list: ["developer", "admin", "corporate", "mastercorporate"], + }, - corporate: { - perm: "editCorporate", - list: ["admin", "developer"], - }, - mastercorporate: { - perm: undefined, - list: ["admin", "developer"], - }, + corporate: { + perm: "editCorporate", + list: ["developer", "admin", "mastercorporate"], + }, + mastercorporate: { + perm: undefined, + list: ["admin", "developer"], + }, - admin: { - perm: "editAdmin", - list: ["developer", "admin"], - }, + admin: { + perm: "editAdmin", + list: ["developer", "admin"], + }, - agent: { - perm: "editCountryManager", - list: ["developer", "admin"], - }, - developer: { - perm: undefined, - list: ["developer"], - }, - }, - updateExpiryDate: { - student: ["developer", "admin"], - teacher: ["developer", "admin"], - corporate: ["admin", "developer"], - mastercorporate: ["admin", "developer"], + agent: { + perm: "editCountryManager", + list: ["developer", "admin"], + }, + developer: { + perm: undefined, + list: ["developer"], + }, + }, + updateExpiryDate: { + student: ["developer", "admin"], + teacher: ["developer", "admin"], + corporate: ["admin", "developer"], + mastercorporate: ["admin", "developer"], - admin: ["developer", "admin"], - agent: ["developer", "admin"], - developer: ["developer"], - }, - examManagement: { - delete: ["developer", "admin"], - }, + admin: ["developer", "admin"], + agent: ["developer", "admin"], + developer: ["developer"], + }, + examManagement: { + delete: ["developer", "admin"], + }, }; diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index e17f5281..cee8393a 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -34,15 +34,15 @@ const USER_TYPE_PERMISSIONS: { }, agent: { perm: "createCodeCountryManager", - list: [], + list: ["student", "teacher", "corporate", "mastercorporate"], }, corporate: { perm: "createCodeCorporate", - list: ["student", "teacher"], + list: ["student", "teacher", "corporate"], }, mastercorporate: { perm: undefined, - list: ["student", "teacher", "corporate"], + list: ["student", "teacher", "corporate", "mastercorporate"], }, admin: { perm: "createCodeAdmin", diff --git a/src/pages/(admin)/BatchCreateUser.tsx b/src/pages/(admin)/BatchCreateUser.tsx index d0d50c57..5335c085 100644 --- a/src/pages/(admin)/BatchCreateUser.tsx +++ b/src/pages/(admin)/BatchCreateUser.tsx @@ -11,10 +11,11 @@ import Modal from "@/components/Modal"; import {BsQuestionCircleFill} from "react-icons/bs"; import {PermissionType} from "@/interfaces/permissions"; import moment from "moment"; -import {checkAccess} from "@/utils/permissions"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; import Checkbox from "@/components/Low/Checkbox"; import ReactDatePicker from "react-datepicker"; import clsx from "clsx"; +import usePermissions from "@/hooks/usePermissions"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); type Type = Exclude; @@ -26,7 +27,7 @@ const USER_TYPE_LABELS: {[key in Type]: string} = { }; const USER_TYPE_PERMISSIONS: { - [key in Type]: {perm: PermissionType | undefined; list: Type[]}; + [key in UserType]: {perm: PermissionType | undefined; list: UserType[]}; } = { student: { perm: "createCodeStudent", @@ -36,9 +37,25 @@ const USER_TYPE_PERMISSIONS: { perm: "createCodeTeacher", list: [], }, + agent: { + perm: "createCodeCountryManager", + list: ["student", "teacher", "corporate", "mastercorporate"], + }, corporate: { perm: "createCodeCorporate", - list: ["student", "teacher"], + list: ["student", "teacher", "corporate"], + }, + mastercorporate: { + perm: undefined, + list: ["student", "teacher", "corporate", "mastercorporate"], + }, + admin: { + perm: "createCodeAdmin", + list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"], + }, + developer: { + perm: undefined, + list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"], }, }; @@ -65,6 +82,7 @@ export default function BatchCreateUser({user}: {user: User}) { const [showHelp, setShowHelp] = useState(false); const {users} = useUsers(); + const {permissions} = usePermissions(user?.id || ""); const {openFilePicker, filesContent, clear} = useFilePicker({ accept: ".xlsx", @@ -214,11 +232,17 @@ export default function BatchCreateUser({user}: {user: User}) { defaultValue="student" onChange={(e) => setType(e.target.value as Type)} className="flex min-h-[70px] w-full min-w-[350px] cursor-pointer justify-center rounded-full border bg-white p-6 text-sm font-normal focus:outline-none"> - {Object.keys(USER_TYPE_LABELS).map((type) => ( - - ))} + {Object.keys(USER_TYPE_LABELS) + .filter((x) => { + const {list, perm} = USER_TYPE_PERMISSIONS[x as Type]; + // if (x === "corporate") console.log(list, perm, checkAccess(user, list, permissions, perm)); + return checkAccess(user, getTypesOfUser(list), permissions, perm); + }) + .map((type) => ( + + ))} )} + ) as any, + cell: (info) => info.getValue() || "N/A", + }), columnHelper.accessor("corporateInformation.companyInformation.name", { header: ( - - - Don't have an account?{" "} - - Sign up - - - - )} - {user && !user.isVerified && ( - - )} - - - - ); + return ( + <> + + Login | EnCoach + + + + +
+ +
+ {/*
*/} + People smiling looking at a tablet +
+
+
+ EnCoach's Logo +

Login to your account

+

with your registered Email Address

+
+ + {!user && ( + <> +
+ setEmail(e.toLowerCase())} placeholder="Enter email address" /> + setPassword(e)} placeholder="Password" /> +
+
setRememberPassword((prev) => !prev)}> + +
+ +
+ Remember my password +
+ + Forgot Password? + +
+ +
+ + Don't have an account?{" "} + + Sign up + + + + )} + {user && !user.isVerified && } +
+
+ + ); } diff --git a/src/pages/register.tsx b/src/pages/register.tsx index 1da21305..3cfda76d 100644 --- a/src/pages/register.tsx +++ b/src/pages/register.tsx @@ -55,8 +55,7 @@ export default function Register({code: queryCode}: {code: string}) {
-
- People smiling looking at a tablet + People smiling looking at a tablet
From 1bb540589498b00941946693e8641af45830fc0f Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 23 Aug 2024 12:02:35 +0100 Subject: [PATCH 6/9] ENCOA-88: Create individual accounts --- src/dashboards/IconCard.tsx | 4 +- src/pages/(admin)/UserCreator.tsx | 266 ++++++++++++++++++++++++++++++ src/pages/api/make_user.ts | 16 +- src/pages/settings.tsx | 55 +++++- 4 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 src/pages/(admin)/UserCreator.tsx diff --git a/src/dashboards/IconCard.tsx b/src/dashboards/IconCard.tsx index 5fd55486..b1ce3c7a 100644 --- a/src/dashboards/IconCard.tsx +++ b/src/dashboards/IconCard.tsx @@ -6,11 +6,12 @@ interface Props { label: string; value?: string | number; color: "purple" | "rose" | "red" | "green"; + className?: string; tooltip?: string; onClick?: () => void; } -export default function IconCard({Icon, label, value, color, tooltip, onClick}: Props) { +export default function IconCard({Icon, label, value, color, tooltip, className, onClick}: Props) { const colorClasses: {[key in typeof color]: string} = { purple: "text-mti-purple-light", red: "text-mti-red-light", @@ -24,6 +25,7 @@ export default function IconCard({Icon, label, value, color, tooltip, onClick}: className={clsx( "bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center text-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300", tooltip && "tooltip tooltip-bottom", + className, )} data-tip={tooltip}> diff --git a/src/pages/(admin)/UserCreator.tsx b/src/pages/(admin)/UserCreator.tsx new file mode 100644 index 00000000..a0b955f5 --- /dev/null +++ b/src/pages/(admin)/UserCreator.tsx @@ -0,0 +1,266 @@ +import Button from "@/components/Low/Button"; +import Checkbox from "@/components/Low/Checkbox"; +import {PERMISSIONS} from "@/constants/userPermissions"; +import {CorporateUser, TeacherUser, Type, User} from "@/interfaces/user"; +import {USER_TYPE_LABELS} from "@/resources/user"; +import axios from "axios"; +import clsx from "clsx"; +import {capitalize, uniqBy} from "lodash"; +import moment from "moment"; +import {useEffect, useState} from "react"; +import ReactDatePicker from "react-datepicker"; +import {toast} from "react-toastify"; +import ShortUniqueId from "short-unique-id"; +import {checkAccess, getTypesOfUser} from "@/utils/permissions"; +import {PermissionType} from "@/interfaces/permissions"; +import usePermissions from "@/hooks/usePermissions"; +import Input from "@/components/Low/Input"; +import CountrySelect from "@/components/Low/CountrySelect"; +import useGroups from "@/hooks/useGroups"; +import useUsers from "@/hooks/useUsers"; +import {getUserName} from "@/utils/users"; +import Select from "@/components/Low/Select"; + +const USER_TYPE_PERMISSIONS: { + [key in Type]: {perm: PermissionType | undefined; list: Type[]}; +} = { + student: { + perm: "createCodeStudent", + list: [], + }, + teacher: { + perm: "createCodeTeacher", + list: [], + }, + agent: { + perm: "createCodeCountryManager", + list: ["student", "teacher", "corporate", "mastercorporate"], + }, + corporate: { + perm: "createCodeCorporate", + list: ["student", "teacher"], + }, + mastercorporate: { + perm: undefined, + list: ["student", "teacher", "corporate"], + }, + admin: { + perm: "createCodeAdmin", + list: ["student", "teacher", "agent", "corporate", "admin", "mastercorporate"], + }, + developer: { + perm: undefined, + list: ["student", "teacher", "agent", "corporate", "admin", "developer", "mastercorporate"], + }, +}; + +export default function UserCreator({user}: {user: User}) { + const [name, setName] = useState(); + const [email, setEmail] = useState(); + const [phone, setPhone] = useState(); + const [passportID, setPassportID] = useState(); + const [studentID, setStudentID] = useState(); + const [country, setCountry] = useState(user?.demographicInformation?.country); + const [group, setGroup] = useState(); + const [availableCorporates, setAvailableCorporates] = useState([]); + const [selectedCorporate, setSelectedCorporate] = useState(); + const [password, setPassword] = useState(); + const [confirmPassword, setConfirmPassword] = useState(); + const [expiryDate, setExpiryDate] = useState( + user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null, + ); + const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [type, setType] = useState("student"); + + const {permissions} = usePermissions(user?.id || ""); + const {groups} = useGroups({admin: ["developer", "admin"].includes(user?.type) ? undefined : user?.id, userType: user?.type}); + const {users} = useUsers(); + + useEffect(() => { + if (!isExpiryDateEnabled) setExpiryDate(null); + }, [isExpiryDateEnabled]); + + useEffect(() => { + setAvailableCorporates( + uniqBy( + users.filter((u) => u.type === "corporate" && groups.flatMap((g) => g.participants).includes(u.id)), + "id", + ), + ); + }, [users, groups]); + + const createUser = () => { + if (!name || name.trim().length === 0) return toast.error("Please enter a valid name!"); + if (!email || email.trim().length === 0) return toast.error("Please enter a valid e-mail address!"); + if (users.map((x) => x.email).includes(email.trim())) return toast.error("That e-mail is already in use!"); + if (!password || password.trim().length === 0) return toast.error("Please enter a valid password!"); + if (password !== confirmPassword) return toast.error("The passwords do not match!"); + + setIsLoading(true); + + const body = { + name, + email, + password, + groupID: group, + type, + studentID: type === "student" ? studentID : undefined, + expiryDate, + demographicInformation: { + passport_id: type === "student" ? passportID : undefined, + phone, + country, + }, + }; + + axios + .post("/api/make_user", body) + .then(() => { + toast.success("That user has been created!"); + + setName(""); + setEmail(""); + setPhone(""); + setPassportID(""); + setStudentID(""); + setCountry(user?.demographicInformation?.country); + setGroup(null); + setSelectedCorporate(null); + setExpiryDate(user?.subscriptionExpirationDate ? moment(user?.subscriptionExpirationDate).toDate() : null); + setIsExpiryDateEnabled(true); + setType("student"); + }) + .catch(() => toast.error("Something went wrong! Please try again later!")) + .finally(() => setIsLoading(false)); + }; + + return ( +
+
+ + + + + + +
+ + +
+ + + + {type === "student" && ( + <> + + + + )} + + {["student", "teacher"].includes(type) && !["corporate", "teacher"].includes(user?.type) && ( +
+ + (!selectedCorporate ? true : x.admin === selectedCorporate)) + .map((g) => ({value: g.id, label: g.name}))} + onChange={(e) => setGroup(e?.value || undefined)} + /> +
+ +
+ + {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)} + /> + )} + + )} +
+
+ + +
+ ); +} diff --git a/src/pages/api/make_user.ts b/src/pages/api/make_user.ts index bdcb14a2..479157bb 100644 --- a/src/pages/api/make_user.ts +++ b/src/pages/api/make_user.ts @@ -37,19 +37,23 @@ async function post(req: NextApiRequest, res: NextApiResponse) { if (!maker) { return res.status(401).json({ok: false, reason: "You must be logged in to make user!"}); } - const {email, passport_id, type, groupName, expiryDate} = req.body as { + const {email, passport_id, password, type, groupName, groupID, expiryDate} = req.body as { email: string; + password?: string; passport_id: string; type: string; - groupName: string; + groupName?: string; + groupID?: string; expiryDate: null | Date; }; // cleaning data delete req.body.passport_id; delete req.body.groupName; + delete req.body.groupID; delete req.body.expiryDate; + delete req.body.password; - await createUserWithEmailAndPassword(auth, email.toLowerCase(), passport_id) + await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id) .then(async (userCredentials) => { const userId = userCredentials.user.uid; @@ -66,6 +70,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { registrationDate: new Date(), subscriptionExpirationDate: expiryDate || null, }; + await setDoc(doc(db, "users", userId), user); if (type === "corporate") { const defaultTeachersGroup: Group = { @@ -123,6 +128,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) { } } + if (!!groupID) { + const groupSnapshot = await getDoc(doc(db, "groups", groupID)); + await setDoc(groupSnapshot.ref, {participants: [...groupSnapshot.data()!.participants, userId]}, {merge: true}); + } + console.log(`Returning - ${email}`); return res.status(200).json({ok: true}); }) diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 8fc93230..9fdd3781 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -16,6 +16,11 @@ import ExamGenerator from "./(admin)/ExamGenerator"; import BatchCreateUser from "./(admin)/BatchCreateUser"; import {checkAccess, getTypesOfUser} from "@/utils/permissions"; import usePermissions from "@/hooks/usePermissions"; +import {useState} from "react"; +import Modal from "@/components/Modal"; +import IconCard from "@/dashboards/IconCard"; +import {BsCode, BsCodeSquare, BsPeopleFill, BsPersonFill} from "react-icons/bs"; +import UserCreator from "./(admin)/UserCreator"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -46,6 +51,8 @@ export default function Admin() { const {user} = useUser({redirectTo: "/login"}); const {permissions} = usePermissions(user?.id || ""); + const [modalOpen, setModalOpen] = useState(); + return ( <> @@ -60,14 +67,52 @@ export default function Admin() { {user && ( + setModalOpen(undefined)}> + + + setModalOpen(undefined)}> + + + setModalOpen(undefined)}> + + + setModalOpen(undefined)}> + + +
{checkAccess(user, getTypesOfUser(["teacher"]), permissions, "viewCodes") && ( - <> - - - - +
+ setModalOpen("createCode")} + /> + setModalOpen("batchCreateCode")} + /> + setModalOpen("createUser")} + /> + setModalOpen("batchCreateUser")} + /> +
)}
From 33fd6ddf8fbf78bd97705840039c287b5d826acc Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 23 Aug 2024 12:10:44 +0100 Subject: [PATCH 7/9] Removed the ability to change the user from the list --- src/pages/(admin)/Lists/UserList.tsx | 46 ---------------------------- 1 file changed, 46 deletions(-) diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 4f72bfc9..12219c08 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -181,52 +181,6 @@ export default function UserList({ }; return (
- {checkAccess(user, updateUserPermission.list, permissions, updateUserPermission.perm) && ( - - -
- -
-
- - -
- - - - -
-
-
-
- )} {!row.original.isVerified && checkAccess(user, updateUserPermission.list, permissions, updateUserPermission.perm) && (
verifyAccount(row.original)}> From 3e21538d023dcd824d5024aa968a20d864015999 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Fri, 23 Aug 2024 16:59:08 +0100 Subject: [PATCH 8/9] ENCOA-98: Change the template on the Excel import Function --- src/pages/(admin)/BatchCreateUser.tsx | 4 +++- src/pages/api/make_user.ts | 34 +++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pages/(admin)/BatchCreateUser.tsx b/src/pages/(admin)/BatchCreateUser.tsx index 409b710a..a1d9b856 100644 --- a/src/pages/(admin)/BatchCreateUser.tsx +++ b/src/pages/(admin)/BatchCreateUser.tsx @@ -104,7 +104,7 @@ export default function BatchCreateUser({user}: {user: User}) { const information = uniqBy( rows .map((row) => { - const [firstName, lastName, country, passport_id, email, phone, group, studentID] = row as string[]; + const [firstName, lastName, country, passport_id, email, phone, group, studentID, corporate] = row as string[]; const countryItem = countryCodes.findOne("countryCode" as any, country.toUpperCase()) || countryCodes.all().find((x) => x.countryNameEn.toLowerCase() === country.toLowerCase()); @@ -116,6 +116,7 @@ export default function BatchCreateUser({user}: {user: User}) { type: type, passport_id: passport_id?.toString().trim() || undefined, groupName: group, + corporate, studentID, demographicInformation: { country: countryItem?.countryCode, @@ -184,6 +185,7 @@ export default function BatchCreateUser({user}: {user: User}) { Phone Number Group Name Student ID + {user?.type !== "corporate" && Corporate (e-mail)} diff --git a/src/pages/api/make_user.ts b/src/pages/api/make_user.ts index 479157bb..a8ceab07 100644 --- a/src/pages/api/make_user.ts +++ b/src/pages/api/make_user.ts @@ -4,7 +4,7 @@ import {getFirestore, setDoc, doc, query, collection, where, getDocs, getDoc, de import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {v4} from "uuid"; -import {Group} from "@/interfaces/user"; +import {CorporateUser, Group} from "@/interfaces/user"; import {createUserWithEmailAndPassword, getAuth} from "firebase/auth"; const DEFAULT_DESIRED_LEVELS = { @@ -37,13 +37,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) { if (!maker) { return res.status(401).json({ok: false, reason: "You must be logged in to make user!"}); } - const {email, passport_id, password, type, groupName, groupID, expiryDate} = req.body as { + const {email, passport_id, password, type, groupName, groupID, expiryDate, corporate} = req.body as { email: string; password?: string; passport_id: string; type: string; groupName?: string; groupID?: string; + corporate?: string; expiryDate: null | Date; }; // cleaning data @@ -52,6 +53,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { delete req.body.groupID; delete req.body.expiryDate; delete req.body.password; + delete req.body.corporate; await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id) .then(async (userCredentials) => { @@ -102,6 +104,34 @@ async function post(req: NextApiRequest, res: NextApiResponse) { await setDoc(doc(db, "groups", defaultCorporateGroup.id), defaultCorporateGroup); } + if (!!corporate) { + const corporateQ = query(collection(db, "users"), where("email", "==", corporate)); + const corporateSnapshot = await getDocs(corporateQ); + + if (!corporateSnapshot.empty) { + const corporateUser = corporateSnapshot.docs[0].data() as CorporateUser; + + const q = query( + collection(db, "groups"), + where("admin", "==", corporateUser.id), + where("name", "==", type === "student" ? "Students" : "Teachers"), + limit(1), + ); + const snapshot = await getDocs(q); + + if (!snapshot.empty) { + const doc = snapshot.docs[0]; + const participants: string[] = doc.get("participants"); + + if (!participants.includes(userId)) { + updateDoc(doc.ref, { + participants: [...participants, userId], + }); + } + } + } + } + if (typeof groupName === "string" && groupName.trim().length > 0) { const q = query(collection(db, "groups"), where("admin", "==", maker.id), where("name", "==", groupName.trim()), limit(1)); const snapshot = await getDocs(q); From 032d20b4b271d084df517c763df208ab88600034 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 24 Aug 2024 01:02:34 +0100 Subject: [PATCH 9/9] ENCOA-96: License Distribuition system from Master Corporate to Corporate --- src/components/Low/Input.tsx | 3 + src/components/UserCard.tsx | 65 ++++++-- src/dashboards/Corporate.tsx | 13 +- src/dashboards/MasterCorporate.tsx | 10 +- src/hooks/useUserBalance.tsx | 21 +++ src/pages/(admin)/Lists/UserList.tsx | 5 + src/pages/api/users/balance.ts | 17 +++ src/utils/codes.be.ts | 10 ++ src/utils/groups.be.ts | 219 ++++++++++----------------- src/utils/users.be.ts | 66 ++++---- 10 files changed, 235 insertions(+), 194 deletions(-) create mode 100644 src/hooks/useUserBalance.tsx create mode 100644 src/pages/api/users/balance.ts create mode 100644 src/utils/codes.be.ts diff --git a/src/components/Low/Input.tsx b/src/components/Low/Input.tsx index f37f9a4c..f5e3cbc1 100644 --- a/src/components/Low/Input.tsx +++ b/src/components/Low/Input.tsx @@ -11,6 +11,7 @@ interface Props { value?: string | number; className?: string; disabled?: boolean; + max?: number; name: string; onChange: (value: string) => void; } @@ -23,6 +24,7 @@ export default function Input({ required = false, value, defaultValue, + max, className, roundness = "full", disabled = false, @@ -72,6 +74,7 @@ export default function Input({ name={name} disabled={disabled} value={value} + max={max} onChange={(e) => onChange(e.target.value)} min={type === "number" ? 0 : undefined} placeholder={placeholder} diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 3efd3ac2..c5d1c273 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -41,6 +41,7 @@ interface Props { onViewStudents?: () => void; onViewTeachers?: () => void; onViewCorporate?: () => void; + maxUserAmount?: number; disabled?: boolean; disabledFields?: { countryManager?: boolean; @@ -72,17 +73,31 @@ const CURRENCIES_OPTIONS = CURRENCIES.map(({label, currency}) => ({ label, })); -const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, onViewCorporate, disabled = false, disabledFields = {}}: Props) => { +const UserCard = ({ + user, + loggedInUser, + maxUserAmount, + onClose, + onViewStudents, + onViewTeachers, + onViewCorporate, + disabled = false, + disabledFields = {}, +}: Props) => { const [expiryDate, setExpiryDate] = useState(user.subscriptionExpirationDate); const [type, setType] = useState(user.type); const [status, setStatus] = useState(user.status); const [referralAgentLabel, setReferralAgentLabel] = useState(); - const [position, setPosition] = useState(user.type === "corporate" ? user.demographicInformation?.position : undefined); + const [position, setPosition] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : undefined, + ); const [studentID, setStudentID] = useState(user.type === "student" ? user.studentID : undefined); - const [referralAgent, setReferralAgent] = useState(user.type === "corporate" ? user.corporateInformation?.referralAgent : undefined); + const [referralAgent, setReferralAgent] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.referralAgent : undefined, + ); const [companyName, setCompanyName] = useState( - user.type === "corporate" + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.companyInformation.name : user.type === "agent" ? user.agentInformation?.companyName @@ -92,11 +107,21 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, const [commercialRegistration, setCommercialRegistration] = useState( user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); - const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined); - const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined); - const [paymentCurrency, setPaymentCurrency] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.currency : "EUR"); - const [monthlyDuration, setMonthlyDuration] = useState(user.type === "corporate" ? user.corporateInformation?.monthlyDuration : undefined); - const [commissionValue, setCommission] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.commission : undefined); + const [userAmount, setUserAmount] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.companyInformation.userAmount : undefined, + ); + const [paymentValue, setPaymentValue] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.value : undefined, + ); + const [paymentCurrency, setPaymentCurrency] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.currency : "EUR", + ); + const [monthlyDuration, setMonthlyDuration] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.monthlyDuration : undefined, + ); + const [commissionValue, setCommission] = useState( + user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.commission : undefined, + ); const {stats} = useStats(user.id); const {users} = useUsers(); const {codes} = useCodes(user.id); @@ -115,7 +140,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, }, [users, referralAgent]); const updateUser = () => { - if (user.type === "corporate" && (!paymentValue || paymentValue < 0)) + if (user.type === "corporate" || (user.type === "mastercorporate" && (!paymentValue || paymentValue < 0))) return toast.error("Please set a price for the user's package before updating!"); if (!confirm(`Are you sure you want to update ${user.name}'s account?`)) return; @@ -179,7 +204,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, ]; const corporateProfileItems = - user.type === "corporate" + user.type === "corporate" || user.type === "mastercorporate" ? [ { icon: , @@ -200,7 +225,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, }; return ( <> - + {user.type === "agent" && ( <> @@ -239,7 +267,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, )} - {user.type === "corporate" && ( + {(user.type === "corporate" || user.type === "mastercorporate") && ( <>
setUserAmount(e ? parseInt(e) : undefined)} placeholder="Enter number of users" defaultValue={userAmount} - disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))} + disabled={ + disabled || + checkAccess( + loggedInUser, + getTypesOfUser(["developer", "admin", ...((user.type === "corporate" ? ["mastercorporate"] : []) as Type[])]), + ) + } />
)} - {user.type === "corporate" && ( + {(user.type === "corporate" || user.type === "mastercorporate") && ( (); const [selectedAssignment, setSelectedAssignment] = useState(); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); - const [userBalance, setUserBalance] = useState(0); const {stats} = useStats(); const {users, reload, isLoading} = useUsers(); const {codes} = useCodes(user.id); const {groups} = useGroups({admin: user.id}); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); + const {balance} = useUserBalance(); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); @@ -174,14 +175,6 @@ export default function CorporateDashboard({user}: Props) { setShowModal(!!selectedUser && page === ""); }, [selectedUser, page]); - useEffect(() => { - const relatedGroups = groups.filter((x) => x.name === "Students" || x.name === "Teachers" || x.name === "Corporate"); - const usersInGroups = relatedGroups.map((x) => x.participants).flat(); - const filteredCodes = codes.filter((x) => !x.userId || !usersInGroups.includes(x.userId)); - - setUserBalance(usersInGroups.length + filteredCodes.length); - }, [codes, groups]); - useEffect(() => { // in this case it fetches the master corporate account getUserCorporate(user.id).then(setCorporateUserToShow); @@ -496,7 +489,7 @@ export default function CorporateDashboard({user}: Props) { u.admin === user.id).flatMap((g) => g.participants))]; @@ -671,7 +672,7 @@ export default function MasterCorporateDashboard({user}: Props) { { setSelectedUser(undefined); diff --git a/src/hooks/useUserBalance.tsx b/src/hooks/useUserBalance.tsx new file mode 100644 index 00000000..2fb5ccfe --- /dev/null +++ b/src/hooks/useUserBalance.tsx @@ -0,0 +1,21 @@ +import {Code, Group, User} from "@/interfaces/user"; +import axios from "axios"; +import {useEffect, useState} from "react"; + +export default function useUserBalance() { + const [balance, setBalance] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get<{balance: number}>(`/api/users/balance`) + .then((response) => setBalance(response.data.balance)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, []); + + return {balance, isLoading, isError, reload: getData}; +} diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 12219c08..5aa8526e 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -27,6 +27,7 @@ import {exportListToExcel, UserListRow} from "@/utils/users"; import {checkAccess} from "@/utils/permissions"; import {PermissionType} from "@/interfaces/permissions"; import usePermissions from "@/hooks/usePermissions"; +import useUserBalance from "@/hooks/useUserBalance"; const columnHelper = createColumnHelper(); const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; @@ -58,6 +59,7 @@ export default function UserList({ const {users, reload} = useUsers(); const {permissions} = usePermissions(user?.id || ""); + const {balance} = useUserBalance(); const {groups} = useGroups({ admin: user && ["corporate", "teacher", "mastercorporate"].includes(user?.type) ? user.id : undefined, userType: user?.type, @@ -551,6 +553,9 @@ export default function UserList({ return (
0 diff --git a/src/pages/api/users/balance.ts b/src/pages/api/users/balance.ts new file mode 100644 index 00000000..d0f1956c --- /dev/null +++ b/src/pages/api/users/balance.ts @@ -0,0 +1,17 @@ +// 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 {getUserBalance} from "@/utils/users.be"; + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const balance = await getUserBalance(req.session.user); + res.status(200).json({balance}); +} diff --git a/src/utils/codes.be.ts b/src/utils/codes.be.ts new file mode 100644 index 00000000..8dd19971 --- /dev/null +++ b/src/utils/codes.be.ts @@ -0,0 +1,10 @@ +import {app} from "@/firebase"; +import {Code} from "@/interfaces/user"; +import {collection, getDocs, getFirestore, query, where} from "firebase/firestore"; + +const db = getFirestore(app); + +export const getUserCodes = async (id: string): Promise => { + const codeDocs = await getDocs(query(collection(db, "codes"), where("creator", "==", id))); + return codeDocs.docs.map((x) => ({...(x.data() as Code), id})) as Code[]; +}; diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index d1a57f72..4ece42aa 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -1,173 +1,112 @@ -import { app } from "@/firebase"; -import { - CorporateUser, - Group, - StudentUser, - TeacherUser, -} from "@/interfaces/user"; -import { - collection, - doc, - getDoc, - getDocs, - getFirestore, - query, - setDoc, - where, -} from "firebase/firestore"; +import {app} from "@/firebase"; +import {CorporateUser, Group, StudentUser, TeacherUser} from "@/interfaces/user"; +import {collection, doc, getDoc, getDocs, getFirestore, query, setDoc, where} from "firebase/firestore"; import moment from "moment"; -import { getUser } from "./users.be"; -import { getSpecificUsers } from "./users.be"; +import {getUser} from "./users.be"; +import {getSpecificUsers} from "./users.be"; const db = getFirestore(app); -export const updateExpiryDateOnGroup = async ( - participantID: string, - corporateID: string -) => { - const corporateRef = await getDoc(doc(db, "users", corporateID)); - const participantRef = await getDoc(doc(db, "users", participantID)); +export const updateExpiryDateOnGroup = async (participantID: string, corporateID: string) => { + const corporateRef = await getDoc(doc(db, "users", corporateID)); + const participantRef = await getDoc(doc(db, "users", participantID)); - if (!corporateRef.exists() || !participantRef.exists()) return; + if (!corporateRef.exists() || !participantRef.exists()) return; - const corporate = { - ...corporateRef.data(), - id: corporateRef.id, - } as CorporateUser; - const participant = { ...participantRef.data(), id: participantRef.id } as - | StudentUser - | TeacherUser; + const corporate = { + ...corporateRef.data(), + id: corporateRef.id, + } as CorporateUser; + const participant = {...participantRef.data(), id: participantRef.id} as StudentUser | TeacherUser; - if ( - corporate.type !== "corporate" || - (participant.type !== "student" && participant.type !== "teacher") - ) - return; + if (corporate.type !== "corporate" || (participant.type !== "student" && participant.type !== "teacher")) return; - if ( - !corporate.subscriptionExpirationDate || - !participant.subscriptionExpirationDate - ) { - return await setDoc( - doc(db, "users", participant.id), - { subscriptionExpirationDate: null }, - { merge: true } - ); - } + if (!corporate.subscriptionExpirationDate || !participant.subscriptionExpirationDate) { + return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: null}, {merge: true}); + } - 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 setDoc( - doc(db, "users", participant.id), - { subscriptionExpirationDate: corporateDate.toISOString() }, - { merge: true } - ); + if (corporateDate.isAfter(participantDate)) + return await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: corporateDate.toISOString()}, {merge: true}); - return; + return; }; export const getGroups = async () => { - const groupDocs = await getDocs(collection(db, "groups")); - return groupDocs.docs.map((x) => ({ ...x.data(), id: x.id })) as Group[]; + const groupDocs = await getDocs(collection(db, "groups")); + return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[]; }; export const getUserGroups = async (id: string): Promise => { - const groupDocs = await getDocs( - query(collection(db, "groups"), where("admin", "==", id)) - ); - return groupDocs.docs.map((x) => ({ ...x.data(), id })) as Group[]; + const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); + return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[]; }; -export const getAllAssignersByCorporate = async ( - corporateID: string -): Promise => { - const groups = await getUserGroups(corporateID); - const groupUsers = ( - await Promise.all( - groups.map(async (g) => await Promise.all(g.participants.map(getUser))) - ) - ).flat(); - const teacherPromises = await Promise.all( - groupUsers.map(async (u) => - u.type === "teacher" - ? u.id - : u.type === "corporate" - ? [...(await getAllAssignersByCorporate(u.id)), u.id] - : undefined - ) - ); +export const getAllAssignersByCorporate = async (corporateID: string): Promise => { + const groups = await getUserGroups(corporateID); + const groupUsers = (await Promise.all(groups.map(async (g) => await Promise.all(g.participants.map(getUser))))).flat(); + const teacherPromises = await Promise.all( + groupUsers.map(async (u) => + u.type === "teacher" ? u.id : u.type === "corporate" ? [...(await getAllAssignersByCorporate(u.id)), u.id] : undefined, + ), + ); - return teacherPromises.filter((x) => !!x).flat() as string[]; + return teacherPromises.filter((x) => !!x).flat() as string[]; }; -export const getGroupsForUser = async (admin: string, participant: string) => { - try { - const queryConstraints = [ - ...(admin ? [where("admin", "==", admin)] : []), - ...(participant - ? [where("participants", "array-contains", participant)] - : []), - ]; - const snapshot = await getDocs( - queryConstraints.length > 0 - ? query(collection(db, "groups"), ...queryConstraints) - : collection(db, "groups") - ); - const groups = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as Group[]; +export const getGroupsForUser = async (admin: string, participant?: string) => { + try { + const queryConstraints = [ + ...(admin ? [where("admin", "==", admin)] : []), + ...(participant ? [where("participants", "array-contains", participant)] : []), + ]; + const snapshot = await getDocs(queryConstraints.length > 0 ? query(collection(db, "groups"), ...queryConstraints) : collection(db, "groups")); + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; - return groups; - } catch (e) { - console.error(e); - return []; - } + return groups; + } catch (e) { + console.error(e); + return []; + } }; -export const getStudentGroupsForUsersWithoutAdmin = async ( - admin: string, - participants: string[] -) => { - try { - const queryConstraints = [ - ...(admin ? [where("admin", "!=", admin)] : []), - ...(participants - ? [where("participants", "array-contains-any", participants)] - : []), - where("name", "==", "Students"), - ]; - const snapshot = await getDocs( - queryConstraints.length > 0 - ? query(collection(db, "groups"), ...queryConstraints) - : collection(db, "groups") - ); - const groups = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as Group[]; +export const getStudentGroupsForUsersWithoutAdmin = async (admin: string, participants: string[]) => { + try { + const queryConstraints = [ + ...(admin ? [where("admin", "!=", admin)] : []), + ...(participants ? [where("participants", "array-contains-any", participants)] : []), + where("name", "==", "Students"), + ]; + const snapshot = await getDocs(queryConstraints.length > 0 ? query(collection(db, "groups"), ...queryConstraints) : collection(db, "groups")); + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; - return groups; - } catch (e) { - console.error(e); - return []; - } + return groups; + } catch (e) { + console.error(e); + return []; + } }; 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); + const adminUserIds = [...new Set(groups.map((g) => g.admin))]; + const adminUsersData = await getSpecificUsers(adminUserIds); - 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 ""; }; diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index 2f3bd51c..334a5d8e 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -1,43 +1,55 @@ -import { app } from "@/firebase"; +import {app} from "@/firebase"; -import { - collection, - doc, - getDoc, - getDocs, - getFirestore, - query, - where, -} from "firebase/firestore"; -import { User } from "@/interfaces/user"; +import {collection, doc, getDoc, getDocs, getFirestore, query, where} from "firebase/firestore"; +import {CorporateUser, Group, User} from "@/interfaces/user"; +import {getGroupsForUser} from "./groups.be"; +import {uniq, uniqBy} from "lodash"; +import {getUserCodes} from "./codes.be"; const db = getFirestore(app); export async function getUsers() { - const snapshot = await getDocs(collection(db, "users")); + const snapshot = await getDocs(collection(db, "users")); - return snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as User[]; + return snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as User[]; } export async function getUser(id: string) { - const userDoc = await getDoc(doc(db, "users", id)); + const userDoc = await getDoc(doc(db, "users", id)); - return { ...userDoc.data(), id } as User; + return {...userDoc.data(), id} as User; } export async function getSpecificUsers(ids: string[]) { - if (ids.length === 0) return []; + if (ids.length === 0) return []; - const snapshot = await getDocs( - query(collection(db, "users"), where("id", "in", ids)) - ); + const snapshot = await getDocs(query(collection(db, "users"), where("id", "in", ids))); - const groups = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as User[]; + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as User[]; - return groups; + return groups; +} + +export async function getUserBalance(user: User) { + 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)); + + if (user.type === "corporate") return participants.length + codes.length; + + 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.length + ); }