diff --git a/public/orange-stock-photo.jpg b/public/orange-stock-photo.jpg new file mode 100644 index 00000000..821d0f82 Binary files /dev/null and b/public/orange-stock-photo.jpg differ diff --git a/public/red-stock-photo.jpg b/public/red-stock-photo.jpg new file mode 100644 index 00000000..3d5ab68b Binary files /dev/null and b/public/red-stock-photo.jpg differ 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 ba02b079..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 [passport_id, setPassportID] = useState(user.type === "student" ? user.demographicInformation?.passport_id : 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; @@ -123,6 +148,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, .post<{user?: User; ok?: boolean}>(`/api/users/update?id=${user.id}`, { ...user, subscriptionExpirationDate: expiryDate, + studentID, type, status, agentInformation: @@ -178,7 +204,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, ]; const corporateProfileItems = - user.type === "corporate" + user.type === "corporate" || user.type === "mastercorporate" ? [ { icon: , @@ -199,7 +225,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, }; return ( <> - + {user.type === "agent" && ( <> @@ -238,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} + disabled={ + disabled || + checkAccess( + loggedInUser, + getTypesOfUser(["developer", "admin", ...((user.type === "corporate" ? ["mastercorporate"] : []) as Type[])]), + ) + } /> setMonthlyDuration(e ? parseInt(e) : undefined)} placeholder="Enter monthly duration" defaultValue={monthlyDuration} - disabled={disabled} + disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))} />
@@ -277,7 +313,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, type="number" defaultValue={paymentValue || 0} className="col-span-3" - disabled={disabled} + disabled={disabled || checkAccess(loggedInUser, getTypesOfUser(["developer", "admin"]))} /> null} - placeholder="Enter National ID or Passport number" - value={user.type === "student" ? user.demographicInformation?.passport_id : undefined} - disabled - required - /> +
+ null} + placeholder="Enter National ID or Passport number" + value={user.type === "student" ? user.demographicInformation?.passport_id : undefined} + disabled + required + /> + +
)}
@@ -456,7 +503,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
)} - {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(); @@ -241,6 +241,7 @@ export default function CorporateDashboard({ user }: Props) { isLoading: isAssignmentsLoading, reload: reloadAssignments, } = useAssignments({ corporate: user.id }); + const { balance } = useUserBalance(); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); @@ -249,19 +250,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); @@ -436,7 +424,8 @@ export default function CorporateDashboard({ user }: Props) {

- Active Assignments ({assignments.filter(activeAssignmentFilter).length}) + Active Assignments ( + {assignments.filter(activeAssignmentFilter).length})

{assignments.filter(activeAssignmentFilter).map((a) => ( @@ -451,7 +440,8 @@ export default function CorporateDashboard({ user }: Props) {

- Planned Assignments ({assignments.filter(futureAssignmentFilter).length}) + Planned Assignments ( + {assignments.filter(futureAssignmentFilter).length})

- Archived Assignments ({assignments.filter(archivedAssignmentFilter).length}) + Archived Assignments ( + {assignments.filter(archivedAssignmentFilter).length})

{assignments.filter(archivedAssignmentFilter).map((a) => ( @@ -640,7 +631,7 @@ export default function CorporateDashboard({ user }: Props) { void; isSelected?: boolean; + className?: string; } export default function IconCard({ @@ -18,6 +19,7 @@ export default function IconCard({ color, tooltip, onClick, + className, isSelected, }: Props) { const colorClasses: { [key in typeof color]: string } = { @@ -33,7 +35,8 @@ export default function IconCard({ 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", - isSelected && `border border-solid border-${colorClasses[color]}` + isSelected && `border border-solid border-${colorClasses[color]}`, + className, )} data-tip={tooltip} > diff --git a/src/dashboards/MasterCorporate.tsx b/src/dashboards/MasterCorporate.tsx index ab7f1663..c90659e5 100644 --- a/src/dashboards/MasterCorporate.tsx +++ b/src/dashboards/MasterCorporate.tsx @@ -67,17 +67,17 @@ import { } from "@/components/ui/popover"; import MasterStatistical from "./MasterStatistical"; import { - futureAssignmentFilter, - pastAssignmentFilter, - archivedAssignmentFilter, - activeAssignmentFilter -} from '@/utils/assignments'; + futureAssignmentFilter, + pastAssignmentFilter, + archivedAssignmentFilter, + activeAssignmentFilter, +} from "@/utils/assignments"; +import useUserBalance from "@/hooks/useUserBalance"; interface Props { user: MasterCorporateUser; } - type StudentPerformanceItem = User & { corporate?: CorporateUser; group?: Group; @@ -439,12 +439,13 @@ export default function MasterCorporateDashboard({ user }: Props) { const { users, reload } = useUsers(); const { codes } = useCodes(user.id); const { groups } = useGroups({ admin: user.id, userType: user.type }); + const { balance } = useUserBalance(); - const masterCorporateUserGroups = [ + const masterCorporateUserGroups = useMemo(() => [ ...new Set( groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants) ), - ]; + ], [groups, user.id]); const corporateUserGroups = [ ...new Set(groups.flatMap((g) => g.participants)), @@ -744,7 +745,8 @@ export default function MasterCorporateDashboard({ user }: Props) {

- Active Assignments ({assignments.filter(activeAssignmentFilter).length}) + Active Assignments ( + {assignments.filter(activeAssignmentFilter).length})

{assignments.filter(activeAssignmentFilter).map((a) => ( @@ -759,7 +761,8 @@ export default function MasterCorporateDashboard({ user }: Props) {

- Planned Assignments ({assignments.filter(futureAssignmentFilter).length}) + Planned Assignments ( + {assignments.filter(futureAssignmentFilter).length})

- Archived Assignments ({assignments.filter(archivedAssignmentFilter).length}) + Archived Assignments ( + {assignments.filter(archivedAssignmentFilter).length})

{assignments.filter(archivedAssignmentFilter).map((a) => ( @@ -903,7 +907,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/interfaces/user.ts b/src/interfaces/user.ts index 83608c29..d4699270 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -1,195 +1,167 @@ -import { Module } from "."; -import { InstructorGender, ShuffleMap } from "./exam"; -import { PermissionType } from "./permissions"; +import {Module} from "."; +import {InstructorGender, ShuffleMap} from "./exam"; +import {PermissionType} from "./permissions"; -export type User = - | StudentUser - | TeacherUser - | CorporateUser - | AgentUser - | AdminUser - | DeveloperUser - | MasterCorporateUser; +export type User = StudentUser | TeacherUser | CorporateUser | AgentUser | AdminUser | DeveloperUser | MasterCorporateUser; export type UserStatus = "active" | "disabled" | "paymentDue"; export interface BasicUser { - email: string; - name: string; - profilePicture: string; - id: string; - isFirstLogin: boolean; - focus: "academic" | "general"; - levels: { [key in Module]: number }; - desiredLevels: { [key in Module]: number }; - type: Type; - bio: string; - isVerified: boolean; - subscriptionExpirationDate?: null | Date; - registrationDate?: Date; - status: UserStatus; - permissions: PermissionType[]; - lastLogin?: Date; + email: string; + name: string; + profilePicture: string; + id: string; + isFirstLogin: boolean; + focus: "academic" | "general"; + levels: {[key in Module]: number}; + desiredLevels: {[key in Module]: number}; + type: Type; + bio: string; + isVerified: boolean; + subscriptionExpirationDate?: null | Date; + registrationDate?: Date; + status: UserStatus; + permissions: PermissionType[]; + lastLogin?: Date; } export interface StudentUser extends BasicUser { - type: "student"; - preferredGender?: InstructorGender; - demographicInformation?: DemographicInformation; - preferredTopics?: string[]; + type: "student"; + studentID?: string; + preferredGender?: InstructorGender; + demographicInformation?: DemographicInformation; + preferredTopics?: string[]; } export interface TeacherUser extends BasicUser { - type: "teacher"; - demographicInformation?: DemographicInformation; + type: "teacher"; + demographicInformation?: DemographicInformation; } export interface CorporateUser extends BasicUser { - type: "corporate"; - corporateInformation: CorporateInformation; - demographicInformation?: DemographicCorporateInformation; + type: "corporate"; + corporateInformation: CorporateInformation; + demographicInformation?: DemographicCorporateInformation; } export interface MasterCorporateUser extends BasicUser { - type: "mastercorporate"; - corporateInformation: CorporateInformation; - demographicInformation?: DemographicCorporateInformation; + type: "mastercorporate"; + corporateInformation: CorporateInformation; + demographicInformation?: DemographicCorporateInformation; } export interface AgentUser extends BasicUser { - type: "agent"; - agentInformation: AgentInformation; - demographicInformation?: DemographicInformation; + type: "agent"; + agentInformation: AgentInformation; + demographicInformation?: DemographicInformation; } export interface AdminUser extends BasicUser { - type: "admin"; - demographicInformation?: DemographicInformation; + type: "admin"; + demographicInformation?: DemographicInformation; } export interface DeveloperUser extends BasicUser { - type: "developer"; - preferredGender?: InstructorGender; - demographicInformation?: DemographicInformation; - preferredTopics?: string[]; + type: "developer"; + preferredGender?: InstructorGender; + demographicInformation?: DemographicInformation; + preferredTopics?: string[]; } export interface CorporateInformation { - companyInformation: CompanyInformation; - monthlyDuration: number; - payment?: { - value: number; - currency: string; - commission: number; - }; - referralAgent?: string; + companyInformation: CompanyInformation; + monthlyDuration: number; + payment?: { + value: number; + currency: string; + commission: number; + }; + referralAgent?: string; } export interface AgentInformation { - companyName: string; - commercialRegistration: string; - companyArabName?: string; + companyName: string; + commercialRegistration: string; + companyArabName?: string; } export interface CompanyInformation { - name: string; - userAmount: number; + name: string; + userAmount: number; } export interface DemographicInformation { - country: string; - phone: string; - gender: Gender; - employment: EmploymentStatus; - passport_id?: string; - timezone?: string; + country: string; + phone: string; + gender: Gender; + employment: EmploymentStatus; + passport_id?: string; + timezone?: string; } export interface DemographicCorporateInformation { - country: string; - phone: string; - gender: Gender; - position: string; - timezone?: string; + country: string; + phone: string; + gender: Gender; + position: string; + timezone?: string; } export type Gender = "male" | "female" | "other"; -export type EmploymentStatus = - | "employed" - | "student" - | "self-employed" - | "unemployed" - | "retired" - | "other"; -export const EMPLOYMENT_STATUS: { status: EmploymentStatus; label: string }[] = - [ - { status: "student", label: "Student" }, - { status: "employed", label: "Employed" }, - { status: "unemployed", label: "Unemployed" }, - { status: "self-employed", label: "Self-employed" }, - { status: "retired", label: "Retired" }, - { status: "other", label: "Other" }, - ]; +export type EmploymentStatus = "employed" | "student" | "self-employed" | "unemployed" | "retired" | "other"; +export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [ + {status: "student", label: "Student"}, + {status: "employed", label: "Employed"}, + {status: "unemployed", label: "Unemployed"}, + {status: "self-employed", label: "Self-employed"}, + {status: "retired", label: "Retired"}, + {status: "other", label: "Other"}, +]; export interface Stat { - id: string; - user: string; - exam: string; - exercise: string; - session: string; - date: number; - module: Module; - solutions: any[]; - type: string; - timeSpent?: number; - inactivity?: number; - assignment?: string; - score: { - correct: number; - total: number; - missing: number; - }; - isDisabled?: boolean; - shuffleMaps?: ShuffleMap[]; - pdf?: { - path: string; - version: string; - }; + id: string; + user: string; + exam: string; + exercise: string; + session: string; + date: number; + module: Module; + solutions: any[]; + type: string; + timeSpent?: number; + inactivity?: number; + assignment?: string; + score: { + correct: number; + total: number; + missing: number; + }; + isDisabled?: boolean; + shuffleMaps?: ShuffleMap[]; + pdf?: { + path: string; + version: string; + }; } export interface Group { - admin: string; - name: string; - participants: string[]; - id: string; - disableEditing?: boolean; + admin: string; + name: string; + participants: string[]; + id: string; + disableEditing?: boolean; } export interface Code { - code: string; - creator: string; - expiryDate: Date; - type: Type; - creationDate?: string; - userId?: string; - email?: string; - name?: string; - passport_id?: string; + code: string; + creator: string; + expiryDate: Date; + type: Type; + creationDate?: string; + userId?: string; + email?: string; + name?: string; + passport_id?: string; } -export type Type = - | "student" - | "teacher" - | "corporate" - | "admin" - | "developer" - | "agent" - | "mastercorporate"; -export const userTypes: Type[] = [ - "student", - "teacher", - "corporate", - "admin", - "developer", - "agent", - "mastercorporate", -]; +export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate"; +export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index e17f5281..313264eb 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -19,6 +19,7 @@ import {BsFileEarmarkEaselFill, BsQuestionCircleFill} from "react-icons/bs"; import {checkAccess, getTypesOfUser} from "@/utils/permissions"; import {PermissionType} from "@/interfaces/permissions"; 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]+)*$/); const USER_TYPE_PERMISSIONS: { @@ -34,7 +35,7 @@ const USER_TYPE_PERMISSIONS: { }, agent: { perm: "createCodeCountryManager", - list: [], + list: ["student", "teacher", "corporate", "mastercorporate"], }, corporate: { perm: "createCodeCorporate", @@ -85,7 +86,7 @@ export default function BatchCodeGenerator({user}: {user: User}) { const information = uniqBy( rows .map((row) => { - const [firstName, lastName, country, passport_id, email, ...phone] = row as string[]; + const [firstName, lastName, country, passport_id, email, phone] = row as string[]; return EMAIL_REGEX.test(email.toString().trim()) ? { email: email.toString().trim().toLowerCase(), diff --git a/src/pages/(admin)/BatchCreateUser.tsx b/src/pages/(admin)/BatchCreateUser.tsx index d0d50c57..a1d9b856 100644 --- a/src/pages/(admin)/BatchCreateUser.tsx +++ b/src/pages/(admin)/BatchCreateUser.tsx @@ -11,10 +11,13 @@ 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"; +import countryCodes from "country-codes-list"; + 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 +29,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,10 +39,26 @@ const USER_TYPE_PERMISSIONS: { 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 BatchCreateUser({user}: {user: User}) { @@ -65,6 +84,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", @@ -84,7 +104,11 @@ export default function BatchCreateUser({user}: {user: User}) { const information = uniqBy( rows .map((row) => { - const [firstName, lastName, country, passport_id, email, phone, group] = 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()); + return EMAIL_REGEX.test(email.toString().trim()) ? { email: email.toString().trim().toLowerCase(), @@ -92,8 +116,10 @@ export default function BatchCreateUser({user}: {user: User}) { type: type, passport_id: passport_id?.toString().trim() || undefined, groupName: group, + corporate, + studentID, demographicInformation: { - country: country, + country: countryItem?.countryCode, passport_id: passport_id?.toString().trim() || undefined, phone, }, @@ -158,6 +184,8 @@ export default function BatchCreateUser({user}: {user: User}) { E-mail Phone Number Group Name + Student ID + {user?.type !== "corporate" && Corporate (e-mail)} @@ -214,11 +242,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) => ( + + ))} )} - - - -
- - - - )} {!row.original.isVerified && checkAccess(user, updateUserPermission.list, permissions, updateUserPermission.perm) && (
verifyAccount(row.original)}> @@ -391,6 +347,15 @@ export default function UserList({ ) as any, cell: (info) => USER_TYPE_LABELS[info.getValue()], }), + columnHelper.accessor("studentID", { + header: ( + + ) as any, + cell: (info) => info.getValue() || "N/A", + }), columnHelper.accessor("corporateInformation.companyInformation.name", { header: ( +
+ ); +} diff --git a/src/pages/api/make_user.ts b/src/pages/api/make_user.ts index bdcb14a2..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,19 +37,25 @@ 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, corporate} = req.body as { email: string; + password?: string; passport_id: string; type: string; - groupName: string; + groupName?: string; + groupID?: string; + corporate?: 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; + delete req.body.corporate; - 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 +72,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 = { @@ -97,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); @@ -123,6 +158,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/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/pages/login.tsx b/src/pages/login.tsx index 73582e1a..4d4b6292 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,229 +1,181 @@ /* eslint-disable @next/next/no-img-element */ -import { User } from "@/interfaces/user"; -import { toast, ToastContainer } from "react-toastify"; +import {User} from "@/interfaces/user"; +import {toast, ToastContainer} from "react-toastify"; import axios from "axios"; -import { FormEvent, useEffect, useState } from "react"; +import {FormEvent, useEffect, useState} from "react"; import Head from "next/head"; import useUser from "@/hooks/useUser"; -import { Divider } from "primereact/divider"; +import {Divider} from "primereact/divider"; import Button from "@/components/Low/Button"; -import { BsArrowRepeat, BsCheck } from "react-icons/bs"; +import {BsArrowRepeat, BsCheck} from "react-icons/bs"; import Link from "next/link"; import Input from "@/components/Low/Input"; import clsx from "clsx"; -import { useRouter } from "next/router"; +import {useRouter} from "next/router"; import EmailVerification from "./(auth)/EmailVerification"; -import { withIronSessionSsr } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; -const EMAIL_REGEX = new RegExp( - /^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g, -); +const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g); -export const getServerSideProps = withIronSessionSsr(({ req, res }) => { - const user = req.session.user; +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; - const envVariables: { [key: string]: string } = {}; - Object.keys(process.env) - .filter((x) => x.startsWith("NEXT_PUBLIC")) - .forEach((x: string) => { - envVariables[x] = process.env[x]!; - }); + const envVariables: {[key: string]: string} = {}; + Object.keys(process.env) + .filter((x) => x.startsWith("NEXT_PUBLIC")) + .forEach((x: string) => { + envVariables[x] = process.env[x]!; + }); - if (user && user.isVerified) { - return { + if (user && user.isVerified) { + return { redirect: { destination: "/", permanent: false, - } + }, }; - } + } - return { - props: { user: null, envVariables }, - }; + return { + props: {user: null, envVariables}, + }; }, sessionOptions); export default function Login() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [rememberPassword, setRememberPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [rememberPassword, setRememberPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); + const router = useRouter(); - const { user, mutateUser } = useUser({ - redirectTo: "/", - redirectIfFound: true, - }); + const {user, mutateUser} = useUser({ + redirectTo: "/", + redirectIfFound: true, + }); - useEffect(() => { - if (user && user.isVerified) router.push("/"); - }, [router, user]); + useEffect(() => { + if (user && user.isVerified) router.push("/"); + }, [router, user]); - const forgotPassword = () => { - if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) { - toast.error("Please enter your e-mail to reset your password!", { - toastId: "forgot-invalid-email", - }); - return; - } + const forgotPassword = () => { + if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) { + toast.error("Please enter your e-mail to reset your password!", { + toastId: "forgot-invalid-email", + }); + return; + } - axios - .post<{ ok: boolean }>("/api/reset", { email }) - .then((response) => { - if (response.data.ok) { - toast.success( - "You should receive an e-mail to reset your password!", - { toastId: "forgot-success" }, - ); - return; - } + axios + .post<{ok: boolean}>("/api/reset", {email}) + .then((response) => { + if (response.data.ok) { + toast.success("You should receive an e-mail to reset your password!", {toastId: "forgot-success"}); + return; + } - toast.error("That e-mail address is not connected to an account!", { - toastId: "forgot-error", - }); - }) - .catch(() => - toast.error("That e-mail address is not connected to an account!", { - toastId: "forgot-error", - }), - ); - }; + toast.error("That e-mail address is not connected to an account!", { + toastId: "forgot-error", + }); + }) + .catch(() => + toast.error("That e-mail address is not connected to an account!", { + toastId: "forgot-error", + }), + ); + }; - const login = (e: FormEvent) => { - e.preventDefault(); + const login = (e: FormEvent) => { + e.preventDefault(); - setIsLoading(true); - axios - .post("/api/login", { email, password }) - .then((response) => { - toast.success("You have been logged in!", { - toastId: "login-successful", - }); - mutateUser(response.data); - }) - .catch((e) => { - if (e.response.status === 401) { - toast.error("Wrong login credentials!", { - toastId: "wrong-credentials", - }); - } else { - toast.error("Something went wrong!", { toastId: "server-error" }); - } - setIsLoading(false); - }) - .finally(() => setIsLoading(false)); - }; + setIsLoading(true); + axios + .post("/api/login", {email, password}) + .then((response) => { + toast.success("You have been logged in!", { + toastId: "login-successful", + }); + mutateUser(response.data); + }) + .catch((e) => { + if (e.response.status === 401) { + toast.error("Wrong login credentials!", { + toastId: "wrong-credentials", + }); + } else { + toast.error("Something went wrong!", {toastId: "server-error"}); + } + setIsLoading(false); + }) + .finally(() => setIsLoading(false)); + }; - 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 && ( - - )} -
-
- - ); + 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/record.tsx b/src/pages/record.tsx index 66bc971f..59481dfb 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -22,6 +22,7 @@ import {usePDFDownload} from "@/hooks/usePDFDownload"; import useRecordStore from "@/stores/recordStore"; import useTrainingContentStore from "@/stores/trainingContentStore"; import StatsGridItem from "@/components/StatGridItem"; +import {checkAccess} from "@/utils/permissions"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -68,10 +69,9 @@ export default function History({user}: {user: User}) { const {assignments} = useAssignments({}); const {users} = useUsers(); - const {stats, isLoading: isStatsLoading} = useStats(user?.type === "student" ? user?.id : statsUserId); + const {stats, isLoading: isStatsLoading} = useStats(statsUserId || user?.id); const {groups: allGroups} = useGroups({}); - - const groups = allGroups.filter((x) => x.admin === user.id); + const {groups} = useGroups({admin: user?.id, userType: user?.type}); const setExams = useExamStore((state) => state.setExams); const setShowSolutions = useExamStore((state) => state.setShowSolutions); @@ -82,6 +82,8 @@ export default function History({user}: {user: User}) { const renderPdfIcon = usePDFDownload("stats"); const router = useRouter(); + useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]); + useEffect(() => { if (stats && !isStatsLoading) { setGroupedStats( @@ -197,6 +199,7 @@ export default function History({user}: {user: User}) { const selectableCorporates = [ defaultSelectableCorporate, ...users + .filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id)) .filter((x) => x.type === "corporate") .map((x) => ({ value: x.id, @@ -208,26 +211,14 @@ export default function History({user}: {user: User}) { const getUsersList = (): User[] => { if (selectedCorporate) { - // get groups for that corporate const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate); - - // get the teacher ids for that group const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants); - // // search for groups for these teachers - // const teacherGroups = allGroups.filter((x) => { - // return selectedCorporateGroupsParticipants.includes(x.admin); - // }); - - // const usersList = [ - // ...selectedCorporateGroupsParticipants, - // ...teacherGroups.flatMap((x) => x.participants), - // ]; const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[]; return userListWithUsers.filter((x) => x); } - return users || []; + return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id)); }; const corporateFilteredUserList = getUsersList(); @@ -267,7 +258,7 @@ export default function History({user}: {user: User}) {
- {(user.type === "developer" || user.type === "admin") && !training && ( + {checkAccess(user, ["developer", "admin", "mastercorporate"]) && !training && ( <> 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
diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 80c08fbb..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")} + /> +
)}
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/permissions.ts b/src/utils/permissions.ts index 0831393d..da3d2d59 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -9,12 +9,10 @@ export function checkAccess(user: User, types: Type[], permissions?: PermissionT // if(user.type === '') { if (!user.type) { - console.warn("User type is empty"); return false; } if (types.length === 0) { - console.warn("No types provided"); return false; } 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 + ); }