From 032d20b4b271d084df517c763df208ab88600034 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 24 Aug 2024 01:02:34 +0100 Subject: [PATCH] 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 + ); }