diff --git a/next.config.js b/next.config.js index cdba31d8..2203b000 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const websiteUrl = process.env.NODE_ENV === 'production' ? "https://encoach.com" : "http://localhost:3000"; const nextConfig = { - reactStrictMode: true, + reactStrictMode: false, output: "standalone", async headers() { return [ diff --git a/package.json b/package.json index 99b697be..abc793ef 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "zustand": "^4.3.6" }, "devDependencies": { + "@simbathesailor/use-what-changed": "^2.0.0", "@types/blob-stream": "^0.1.33", "@types/formidable": "^3.4.0", "@types/howler": "^2.2.11", diff --git a/src/dashboards/Corporate.tsx b/src/dashboards/Corporate.tsx index 0afaf496..54091dd8 100644 --- a/src/dashboards/Corporate.tsx +++ b/src/dashboards/Corporate.tsx @@ -1,8 +1,8 @@ /* eslint-disable @next/next/no-img-element */ import Modal from "@/components/Modal"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import useUsers from "@/hooks/useUsers"; -import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; +import useUsers, { userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers"; +import {CorporateUser, Group, MasterCorporateUser, Stat, User } from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; import {dateSorter} from "@/utils"; import moment from "moment"; @@ -156,6 +156,7 @@ const StudentPerformanceList = ({items, stats, users}: {items: StudentPerformanc ); }; + export default function CorporateDashboard({user, linkedCorporate}: Props) { const [selectedUser, setSelectedUser] = useState(); const [showModal, setShowModal] = useState(false); @@ -165,8 +166,8 @@ export default function CorporateDashboard({user, linkedCorporate}: Props) { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const {balance} = useUserBalance(); - const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers({type: "student"}); - const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers({type: "teacher"}); + const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(userHashStudent); + const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(userHashTeacher); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); diff --git a/src/dashboards/CorporateStudentsLevels.tsx b/src/dashboards/CorporateStudentsLevels.tsx index 36355747..203b3caf 100644 --- a/src/dashboards/CorporateStudentsLevels.tsx +++ b/src/dashboards/CorporateStudentsLevels.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from "react"; -import useUsers from "@/hooks/useUsers"; +import useUsers, { userHashStudent, userHashTeacher, userHashCorporate } from "@/hooks/useUsers"; import useGroups from "@/hooks/useGroups"; import {User} from "@/interfaces/user"; import Select from "@/components/Low/Select"; @@ -63,8 +63,8 @@ const Card = ({user}: {user: User}) => { const CorporateStudentsLevels = () => { const [corporateId, setCorporateId] = React.useState(""); - const {users: students} = useUsers({type: "student"}); - const {users: corporates} = useUsers({type: "corporate"}); + const {users: students} = useUsers(userHashStudent); + const {users: corporates} = useUsers(userHashCorporate); const corporate = useMemo(() => corporates.find((u) => u.id === corporateId) || corporates[0], [corporates, corporateId]); diff --git a/src/dashboards/MasterCorporate.tsx b/src/dashboards/MasterCorporate.tsx index 568b6acb..e4a252a2 100644 --- a/src/dashboards/MasterCorporate.tsx +++ b/src/dashboards/MasterCorporate.tsx @@ -1,7 +1,7 @@ /* eslint-disable @next/next/no-img-element */ import Modal from "@/components/Modal"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import useUsers from "@/hooks/useUsers"; +import useUsers, { userHashStudent, userHashTeacher, userHashCorporate } from "@/hooks/useUsers"; import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; import {dateSorter} from "@/utils"; @@ -302,9 +302,9 @@ export default function MasterCorporateDashboard({user}: Props) { const {data: stats} = useFilterRecordsByUser(); - const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers({type: "student"}); - const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers({type: "teacher"}); - const {users: corporates, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers({type: "corporate"}); + const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(userHashStudent); + const {users: teachers, reload: reloadTeachers, isLoading: isTeachersLoading} = useUsers(userHashTeacher); + const {users: corporates, reload: reloadCorporates, isLoading: isCorporatesLoading} = useUsers(userHashCorporate); const {groups} = useGroups({admin: user.id, userType: user.type}); const {balance} = useUserBalance(); diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index ed0a2ea1..1539c0d3 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -6,7 +6,7 @@ import useAssignments from "@/hooks/useAssignments"; import useGradingSystem from "@/hooks/useGrading"; import useInvites from "@/hooks/useInvites"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import useUsers from "@/hooks/useUsers"; +import useUsers, { userHashStudent, userHashTeacher, userHashCorporate} from "@/hooks/useUsers"; import {Invite} from "@/interfaces/invite"; import {Assignment} from "@/interfaces/results"; import {CorporateUser, MasterCorporateUser, Stat, User} from "@/interfaces/user"; @@ -43,8 +43,8 @@ export default function StudentDashboard({user, linkedCorporate}: Props) { const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id}); const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id}); - const {users: teachers} = useUsers({type: "teacher"}); - const {users: corporates} = useUsers({type: "corporate"}); + const {users: teachers} = useUsers(userHashTeacher); + const {users: corporates} = useUsers(userHashCorporate); const users = useMemo(() => [...teachers, ...corporates], [teachers, corporates]); const router = useRouter(); diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index 7ed48271..ca914368 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -1,7 +1,7 @@ /* eslint-disable @next/next/no-img-element */ import Modal from "@/components/Modal"; import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; -import useUsers from "@/hooks/useUsers"; +import useUsers, { userHashStudent, userHashTeacher, userHashCorporate } from "@/hooks/useUsers"; import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; import UserList from "@/pages/(admin)/Lists/UserList"; import {dateSorter} from "@/utils"; @@ -67,7 +67,7 @@ export default function TeacherDashboard({user, linkedCorporate}: Props) { const {permissions} = usePermissions(user.id); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assigner: user.id}); - const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers({type: "student"}); + const {users: students, reload: reloadStudents, isLoading: isStudentsLoading} = useUsers(userHashStudent); const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const router = useRouter(); diff --git a/src/hooks/useUsers.tsx b/src/hooks/useUsers.tsx index 89416930..ff700f39 100644 --- a/src/hooks/useUsers.tsx +++ b/src/hooks/useUsers.tsx @@ -2,10 +2,13 @@ import {Type, User} from "@/interfaces/user"; import Axios from "axios"; import {useEffect, useState} from "react"; import {setupCache} from "axios-cache-interceptor"; - const instance = Axios.create(); const axios = setupCache(instance); +export const userHashStudent = { type: "student" } as { type: Type }; +export const userHashTeacher = { type: "teacher" } as { type: Type }; +export const userHashCorporate = { type: "corporate" } as { type: Type }; + export default function useUsers(props?: {type?: Type; page?: number; size?: number}) { const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index d4699270..a40e8a61 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -164,4 +164,4 @@ export interface Code { } export type Type = "student" | "teacher" | "corporate" | "admin" | "developer" | "agent" | "mastercorporate"; -export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; +export const userTypes: Type[] = ["student", "teacher", "corporate", "admin", "developer", "agent", "mastercorporate"]; \ No newline at end of file diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index e734a7aa..10bc63ad 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -9,7 +9,7 @@ import axios from "axios"; import clsx from "clsx"; import {capitalize, reverse} from "lodash"; import moment from "moment"; -import {Fragment, useEffect, useState} from "react"; +import {Fragment, useEffect, useState, useMemo} from "react"; import {BsArrowDown, BsArrowDownUp, BsArrowUp, BsCheck, BsCheckCircle, BsEye, BsFillExclamationOctagonFill, BsPerson, BsTrash} from "react-icons/bs"; import {toast} from "react-toastify"; import {countries, TCountries} from "countries-list"; @@ -59,7 +59,12 @@ export default function UserList({ const [displayUsers, setDisplayUsers] = useState([]); const [selectedUser, setSelectedUser] = useState(); - const {users, page, total, reload, next, previous} = useUsers({type, size: 25}); + const userHash = useMemo(() => ({ + type, + size: 25, + }), [type]) + + const {users, page, total, reload, next, previous} = useUsers(userHash); const {permissions} = usePermissions(user?.id || ""); const {balance} = useUserBalance(); const {groups} = useGroups({ diff --git a/src/utils/groups.ts b/src/utils/groups.ts index 8416683b..9e20af07 100644 --- a/src/utils/groups.ts +++ b/src/utils/groups.ts @@ -1,37 +1,48 @@ -import {CorporateUser, Group, User, Type} from "@/interfaces/user"; +import { CorporateUser, Group, User, Type } from "@/interfaces/user"; import axios from "axios"; export const isUserFromCorporate = async (userID: string) => { - const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; - const users = (await axios.get("/api/users/list")).data; + const groups = (await axios.get(`/api/groups?participant=${userID}`)) + .data; + const usersData = (await axios.get<{users: User[], total: number}>("/api/users/list")).data; - const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type); - return adminTypes.includes("corporate"); + const adminTypes = groups.reduce((accm: Type[], g) => { + const user = usersData.users.find((u) => u.id === g.admin); + if (user) { + return [...accm, user.type]; + } + + return accm; + }, []); + return adminTypes.includes("corporate"); }; const getAdminForGroup = async (userID: string, role: Type) => { - const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; + const groups = (await axios.get(`/api/groups?participant=${userID}`)) + .data; - const adminRequests = await Promise.all( - groups.map(async (g) => { - const userRequest = await axios.get(`/api/users/${g.admin}`); - if (userRequest.status === 200) return userRequest.data; - return undefined; - }), - ); + const adminRequests = await Promise.all( + groups.map(async (g) => { + const userRequest = await axios.get(`/api/users/${g.admin}`); + if (userRequest.status === 200) return userRequest.data; + return undefined; + }) + ); - const admins = adminRequests.filter((x) => x?.type === role); - return admins.length > 0 ? (admins[0] as CorporateUser) : undefined; + const admins = adminRequests.filter((x) => x?.type === role); + return admins.length > 0 ? (admins[0] as CorporateUser) : undefined; }; -export const getUserCorporate = async (userID: string): Promise => { - const userRequest = await axios.get(`/api/users/${userID}`); - if (userRequest.status === 200) { - const user = userRequest.data; - if (user.type === "corporate") { - return getAdminForGroup(userID, "mastercorporate"); - } - } +export const getUserCorporate = async ( + userID: string +): Promise => { + const userRequest = await axios.get(`/api/users/${userID}`); + if (userRequest.status === 200) { + const user = userRequest.data; + if (user.type === "corporate") { + return getAdminForGroup(userID, "mastercorporate"); + } + } - return getAdminForGroup(userID, "corporate"); + return getAdminForGroup(userID, "corporate"); }; diff --git a/src/utils/users.be.ts b/src/utils/users.be.ts index 535f250e..97b17bef 100644 --- a/src/utils/users.be.ts +++ b/src/utils/users.be.ts @@ -86,6 +86,9 @@ export async function getLinkedUsers(userID?: string, userType?: Type, type?: Ty ...(userType === "teacher" ? belongingGroups.flatMap((x) => x.participants) : []), ]); + // тип [FirebaseError: Invalid Query. A non-empty array is required for 'in' filters.] { + if(participants.length === 0) return {users: [], total: 0}; + const snapshot = await getDocs(query(collection(db, "users"), ...[where(documentId(), "in", participants), ...q])); const users = snapshot.docs.map((doc) => ({ id: doc.id, diff --git a/yarn.lock b/yarn.lock index 407edc16..64f1a8a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1592,6 +1592,11 @@ resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== +"@simbathesailor/use-what-changed@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@simbathesailor/use-what-changed/-/use-what-changed-2.0.0.tgz#7f82d78f92c8588b5fadd702065dde93bd781403" + integrity sha512-ulBNrPSvfho9UN6zS2fii3AsdEcp2fMaKeqUZZeCNPaZbB6aXyTUhpEN9atjMAbu/eyK3AY8L4SYJUG62Ekocw== + "@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" @@ -6450,7 +6455,16 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6514,7 +6528,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7102,8 +7123,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7121,6 +7141,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"