diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx index 502384e1..b8765a97 100644 --- a/src/components/High/Layout.tsx +++ b/src/components/High/Layout.tsx @@ -1,4 +1,3 @@ -import useEntities from "@/hooks/useEntities"; import { EntityWithRoles } from "@/interfaces/entity"; import { User } from "@/interfaces/user"; import clsx from "clsx"; @@ -6,66 +5,126 @@ import { useRouter } from "next/router"; import { ToastContainer } from "react-toastify"; import Navbar from "../Navbar"; import Sidebar from "../Sidebar"; +import React, { useEffect, useState } from "react"; + +export const LayoutContext = React.createContext({ + onFocusLayerMouseEnter: () => {}, + setOnFocusLayerMouseEnter: (() => {}) as React.Dispatch< + React.SetStateAction<() => void> + >, + navDisabled: false, + setNavDisabled: (() => {}) as React.Dispatch>, + focusMode: false, + setFocusMode: (() => {}) as React.Dispatch>, + hideSidebar: false, + setHideSidebar: (() => {}) as React.Dispatch>, + bgColor: "bg-white", + setBgColor: (() => {}) as React.Dispatch>, + className: "", + setClassName: (() => {}) as React.Dispatch>, +}); interface Props { - user: User; - entities?: EntityWithRoles[] - children: React.ReactNode; - className?: string; - navDisabled?: boolean; - focusMode?: boolean; - hideSidebar?: boolean - bgColor?: string; - onFocusLayerMouseEnter?: () => void; + user: User; + entities?: EntityWithRoles[]; + children: React.ReactNode; + refreshPage?: boolean; } export default function Layout({ - user, - children, - className, - bgColor = "bg-white", - hideSidebar, - navDisabled = false, - focusMode = false, - onFocusLayerMouseEnter + user, + entities, + children, + refreshPage, }: Props) { - const router = useRouter(); - const { entities } = useEntities() + const [onFocusLayerMouseEnter, setOnFocusLayerMouseEnter] = useState( + () => () => {} + ); + const [navDisabled, setNavDisabled] = useState(false); + const [focusMode, setFocusMode] = useState(false); + const [hideSidebar, setHideSidebar] = useState(false); + const [bgColor, setBgColor] = useState("bg-white"); + const [className, setClassName] = useState(""); - return ( -
- - {!hideSidebar && user && ( - - )} -
- {!hideSidebar && user && ( - - )} -
- {children} -
-
-
- ); + useEffect(() => { + if (refreshPage) { + setClassName(""); + setBgColor("bg-white"); + setFocusMode(false); + setHideSidebar(false); + setNavDisabled(false); + setOnFocusLayerMouseEnter(() => () => {}); + } + }, [refreshPage]); + + const LayoutContextValue = React.useMemo( + () => ({ + onFocusLayerMouseEnter, + setOnFocusLayerMouseEnter, + navDisabled, + setNavDisabled, + focusMode, + setFocusMode, + hideSidebar, + setHideSidebar, + bgColor, + setBgColor, + className, + setClassName, + }), + [ + bgColor, + className, + focusMode, + hideSidebar, + navDisabled, + onFocusLayerMouseEnter, + ] + ); + + const router = useRouter(); + + return ( + +
+ + {!hideSidebar && user && ( + + )} +
+ {!hideSidebar && user && ( + + )} +
+ {children} +
+
+
+
+ ); } diff --git a/src/components/Low/AsyncSelect.tsx b/src/components/Low/AsyncSelect.tsx index b125b2ad..a839bc23 100644 --- a/src/components/Low/AsyncSelect.tsx +++ b/src/components/Low/AsyncSelect.tsx @@ -48,6 +48,15 @@ export default function AsyncSelect({ flat, }: Props & (MultiProps | SingleProps)) { const [target, setTarget] = useState(); + const [inputValue, setInputValue] = useState(""); + + //Implemented a debounce to prevent the API from being called too frequently + useEffect(() => { + const timer = setTimeout(() => { + loadOptions(inputValue); + }, 200); + return () => clearTimeout(timer); + }, [inputValue, loadOptions]); useEffect(() => { if (document) setTarget(document.body); @@ -77,7 +86,7 @@ export default function AsyncSelect({ filterOption={null} loadingMessage={() => "Loading..."} onInputChange={(inputValue) => { - loadOptions(inputValue); + setInputValue(inputValue); }} options={options} value={value} diff --git a/src/components/Medium/UserProfileSkeleton.tsx b/src/components/Medium/UserProfileSkeleton.tsx new file mode 100644 index 00000000..33b46f3b --- /dev/null +++ b/src/components/Medium/UserProfileSkeleton.tsx @@ -0,0 +1,60 @@ +import React from "react"; + +export default function UserProfileSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[...Array(4)].map((_, i) => ( +
+
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index daffc144..a4b6f39e 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,17 +2,16 @@ import clsx from "clsx"; import { IconType } from "react-icons"; import { MdSpaceDashboard } from "react-icons/md"; import { - BsFileEarmarkText, - BsClockHistory, - BsPencil, - BsGraphUp, - BsChevronBarRight, - BsChevronBarLeft, - BsShieldFill, - BsCloudFill, - BsCurrencyDollar, - BsClipboardData, - BsPeople, + BsFileEarmarkText, + BsClockHistory, + BsGraphUp, + BsChevronBarRight, + BsChevronBarLeft, + BsShieldFill, + BsCloudFill, + BsCurrencyDollar, + BsClipboardData, + BsPeople, } from "react-icons/bs"; import { CiDumbbell } from "react-icons/ci"; import { RiLogoutBoxFill } from "react-icons/ri"; @@ -24,218 +23,454 @@ import { preventNavigation } from "@/utils/navigation.disabled"; import usePreferencesStore from "@/stores/preferencesStore"; import { User } from "@/interfaces/user"; import useTicketsListener from "@/hooks/useTicketsListener"; -import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import { getTypesOfUser } from "@/utils/permissions"; import usePermissions from "@/hooks/usePermissions"; import { EntityWithRoles } from "@/interfaces/entity"; -import { useAllowedEntities, useAllowedEntitiesSomePermissions } from "@/hooks/useEntityPermissions"; +import { + useAllowedEntities, + useAllowedEntitiesSomePermissions, +} from "@/hooks/useEntityPermissions"; import { useMemo } from "react"; +import { PermissionType } from "../interfaces/permissions"; interface Props { - path: string; - navDisabled?: boolean; - focusMode?: boolean; - onFocusLayerMouseEnter?: () => void; - className?: string; - user: User; - entities?: EntityWithRoles[] + path: string; + navDisabled?: boolean; + focusMode?: boolean; + onFocusLayerMouseEnter?: () => void; + className?: string; + user: User; + entities?: EntityWithRoles[]; } interface NavProps { - Icon: IconType; - label: string; - path: string; - keyPath: string; - disabled?: boolean; - isMinimized?: boolean; - badge?: number; + Icon: IconType; + label: string; + path: string; + keyPath: string; + disabled?: boolean; + isMinimized?: boolean; + badge?: number; } -const Nav = ({ Icon, label, path, keyPath, disabled = false, isMinimized = false, badge }: NavProps) => { - return ( - - - {!isMinimized && {label}} - {!!badge && badge > 0 && ( -
- {badge} -
- )} - - ); +const Nav = ({ + Icon, + label, + path, + keyPath, + disabled = false, + isMinimized = false, + badge, +}: NavProps) => { + return ( + + + {!isMinimized && {label}} + {!!badge && badge > 0 && ( +
+ {badge} +
+ )} + + ); }; export default function Sidebar({ - path, - entities = [], - navDisabled = false, - focusMode = false, - user, - onFocusLayerMouseEnter, - className + path, + entities = [], + navDisabled = false, + focusMode = false, + user, + onFocusLayerMouseEnter, + className, }: Props) { - const router = useRouter(); + const router = useRouter(); - const isAdmin = useMemo(() => ['developer', 'admin'].includes(user?.type), [user?.type]) + const isAdmin = useMemo( + () => ["developer", "admin"].includes(user?.type), + [user?.type] + ); - const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]); + const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [ + state.isSidebarMinimized, + state.toggleSidebarMinimized, + ]); - const { totalAssignedTickets } = useTicketsListener(user.id); - const { permissions } = usePermissions(user.id); + const { permissions } = usePermissions(user.id); - const entitiesAllowStatistics = useAllowedEntities(user, entities, "view_statistics") - const entitiesAllowPaymentRecord = useAllowedEntities(user, entities, "view_payment_record") + const entitiesAllowStatistics = useAllowedEntities( + user, + entities, + "view_statistics" + ); + const entitiesAllowPaymentRecord = useAllowedEntities( + user, + entities, + "view_payment_record" + ); - const entitiesAllowGeneration = useAllowedEntitiesSomePermissions(user, entities, [ - "generate_reading", "generate_listening", "generate_writing", "generate_speaking", "generate_level" - ]) + const entitiesAllowGeneration = useAllowedEntitiesSomePermissions( + user, + entities, + [ + "generate_reading", + "generate_listening", + "generate_writing", + "generate_speaking", + "generate_level", + ] + ); - const logout = async () => { - axios.post("/api/logout").finally(() => { - setTimeout(() => router.reload(), 500); - }); - }; + const sidebarPermissions = useMemo<{ [key: string]: boolean }>(() => { + if (user.type === "developer") { + return { + viewExams: true, + viewStats: true, + viewRecords: true, + viewTickets: true, + viewClassrooms: true, + viewSettings: true, + viewPaymentRecord: true, + viewGeneration: true, + }; + } + const sidebarPermissions: { [key: string]: boolean } = { + viewExams: false, + viewStats: false, + viewRecords: false, + viewTickets: false, + viewClassrooms: false, + viewSettings: false, + viewPaymentRecord: false, + viewGeneration: false, + }; - const disableNavigation = preventNavigation(navDisabled, focusMode); + if (!user || !user?.type) return sidebarPermissions; - return ( -
-
-
-
-
+ const neededPermissions = permissions.reduce((acc, curr) => { + if ( + ["viewExams", "viewRecords", "viewTickets"].includes(curr as string) + ) { + acc.push(curr); + } + return acc; + }, [] as PermissionType[]); -
-
- {isMinimized ? : } - {!isMinimized && Minimize} -
-
{ } : logout} - className={clsx( - "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", - isMinimized ? "w-fit" : "w-full min-w-[250px] px-8", - )}> - - {!isMinimized && Log Out} -
-
- {focusMode && } -
- ); + if ( + ["student", "teacher", "developer"].includes(user.type) && + neededPermissions.includes("viewExams") + ) { + sidebarPermissions["viewExams"] = true; + } + if ( + getTypesOfUser(["agent"]).includes(user.type) && + (entitiesAllowStatistics.length > 0 || + neededPermissions.includes("viewStats")) + ) { + sidebarPermissions["viewStats"] = true; + } + if ( + [ + "admin", + "developer", + "teacher", + "corporate", + "mastercorporate", + ].includes(user.type) && + (entitiesAllowGeneration.length > 0 || isAdmin) + ) { + sidebarPermissions["viewGeneration"] = true; + } + if ( + getTypesOfUser(["agent"]).includes(user.type) && + neededPermissions.includes("viewRecords") + ) { + sidebarPermissions["viewRecords"] = true; + } + if ( + ["admin", "developer", "agent"].includes(user.type) && + neededPermissions.includes("viewTickets") + ) { + sidebarPermissions["viewTickets"] = true; + } + if ( + [ + "admin", + "mastercorporate", + "developer", + "corporate", + "teacher", + "student", + ].includes(user.type) + ) { + sidebarPermissions["viewClassrooms"] = true; + } + if (getTypesOfUser(["student", "agent"]).includes(user.type)) { + sidebarPermissions["viewSettings"] = true; + } + if ( + ["admin", "developer", "agent", "corporate", "mastercorporate"].includes( + user.type + ) && + entitiesAllowPaymentRecord.length > 0 + ) { + sidebarPermissions["viewPaymentRecord"] = true; + } + return sidebarPermissions; + }, [ + entitiesAllowGeneration.length, + entitiesAllowPaymentRecord.length, + entitiesAllowStatistics.length, + isAdmin, + permissions, + user, + ]); + + const { totalAssignedTickets } = useTicketsListener( + user.id, + sidebarPermissions["viewTickets"] + ); + + const logout = async () => { + axios.post("/api/logout").finally(() => { + setTimeout(() => router.reload(), 500); + }); + }; + + const disableNavigation = preventNavigation(navDisabled, focusMode); + + return ( +
+
+
+
+
+ +
+
+ {isMinimized ? ( + + ) : ( + + )} + {!isMinimized && ( + Minimize + )} +
+
{} : logout} + className={clsx( + "hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", + isMinimized ? "w-fit" : "w-full min-w-[250px] px-8" + )} + > + + {!isMinimized && ( + Log Out + )} +
+
+ {focusMode && ( + + )} +
+ ); } diff --git a/src/exams/Selection.tsx b/src/exams/Selection.tsx index 64f9f615..848c66c3 100644 --- a/src/exams/Selection.tsx +++ b/src/exams/Selection.tsx @@ -14,8 +14,6 @@ import { BsPen, BsXCircle, } from "react-icons/bs"; -import { totalExamsByModule } from "@/utils/stats"; -import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser"; import Button from "@/components/Low/Button"; import { sortByModuleName } from "@/utils/moduleUtils"; import { capitalize } from "lodash"; @@ -24,7 +22,7 @@ import { Variant } from "@/interfaces/exam"; import useSessions, { Session } from "@/hooks/useSessions"; import SessionCard from "@/components/Medium/SessionCard"; import useExamStore from "@/stores/exam"; -import moment from "moment"; +import useStats from "../hooks/useStats"; interface Props { user: User; @@ -41,7 +39,21 @@ export default function Selection({ user, page, onStart }: Props) { const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true); const [variant, setVariant] = useState("full"); - const { data: stats } = useFilterRecordsByUser(user?.id); + const { + data: { + allStats = [], + moduleCount: { reading, listening, writing, speaking, level } = { + reading: 0, + listening: 0, + writing: 0, + speaking: 0, + level: 0, + }, + }, + } = useStats<{ + allStats: Stat[]; + moduleCount: Record; + }>(user?.id, !user?.id, "byModule"); const { sessions, isLoading, reload } = useSessions(user.id); const dispatch = useExamStore((state) => state.dispatch); @@ -77,7 +89,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Reading", - value: totalExamsByModule(stats, "reading"), + value: reading, tooltip: "The amount of reading exams performed.", }, { @@ -85,7 +97,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Listening", - value: totalExamsByModule(stats, "listening"), + value: listening, tooltip: "The amount of listening exams performed.", }, { @@ -93,7 +105,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Writing", - value: totalExamsByModule(stats, "writing"), + value: writing, tooltip: "The amount of writing exams performed.", }, { @@ -101,7 +113,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Speaking", - value: totalExamsByModule(stats, "speaking"), + value: speaking, tooltip: "The amount of speaking exams performed.", }, { @@ -109,7 +121,7 @@ export default function Selection({ user, page, onStart }: Props) { ), label: "Level", - value: totalExamsByModule(stats, "level"), + value: level, tooltip: "The amount of level exams performed.", }, ]} diff --git a/src/hooks/useEntities.tsx b/src/hooks/useEntities.tsx index 434d5d1f..a9f17094 100644 --- a/src/hooks/useEntities.tsx +++ b/src/hooks/useEntities.tsx @@ -1,23 +1,22 @@ import { EntityWithRoles } from "@/interfaces/entity"; -import { Discount } from "@/interfaces/paypal"; -import { Code, Group, User } from "@/interfaces/user"; import axios from "axios"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; -export default function useEntities() { +export default function useEntities(shouldNot?: boolean) { const [entities, setEntities] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); - const getData = () => { + const getData = useCallback(() => { + if (shouldNot) return; setIsLoading(true); axios .get("/api/entities?showRoles=true") .then((response) => setEntities(response.data)) .finally(() => setIsLoading(false)); - }; + }, [shouldNot]); - useEffect(getData, []); + useEffect(getData, [getData]) return { entities, isLoading, isError, reload: getData }; } diff --git a/src/hooks/useStats.tsx b/src/hooks/useStats.tsx new file mode 100644 index 00000000..1f182469 --- /dev/null +++ b/src/hooks/useStats.tsx @@ -0,0 +1,42 @@ +import axios from "axios"; +import { useCallback, useEffect, useState } from "react"; + +export default function useStats( + id?: string, + shouldNotQuery: boolean = !id, + queryType: string = "stats" +) { + type ElementType = T extends (infer U)[] ? U : never; + + const [data, setData] = useState({} as unknown as T); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = useCallback(() => { + if (shouldNotQuery) return; + + setIsLoading(true); + setIsError(false); + let endpoint = `/api/stats/user/${id}`; + if (queryType) endpoint += `?query=${queryType}`; + axios + .get(endpoint) + .then((response) => { + console.log(response.data); + setData(response.data); + }) + .catch(() => setIsError(true)) + .finally(() => setIsLoading(false)); + }, [id, shouldNotQuery, queryType]); + + useEffect(() => { + getData(); + }, [getData]); + + return { + data, + reload: getData, + isLoading, + isError, + }; +} diff --git a/src/hooks/useTicketsListener.tsx b/src/hooks/useTicketsListener.tsx index a3babb1a..75a552e9 100644 --- a/src/hooks/useTicketsListener.tsx +++ b/src/hooks/useTicketsListener.tsx @@ -1,26 +1,28 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import axios from "axios"; -const useTicketsListener = (userId?: string) => { +const useTicketsListener = (userId?: string, canFetch?: boolean) => { const [assignedTickets, setAssignedTickets] = useState([]); - const getData = () => { + const getData = useCallback(() => { axios .get("/api/tickets/assignedToUser") .then((response) => setAssignedTickets(response.data)); - }; - - useEffect(() => { - getData(); }, []); useEffect(() => { + if (!canFetch) return; + getData(); + }, [canFetch, getData]); + + useEffect(() => { + if (!canFetch) return; const intervalId = setInterval(() => { getData(); }, 60 * 1000); return () => clearInterval(intervalId); - }, [assignedTickets]); + }, [assignedTickets, canFetch, getData]); if (userId) { return { diff --git a/src/interfaces/results.ts b/src/interfaces/results.ts index 58832a4f..ba94076f 100644 --- a/src/interfaces/results.ts +++ b/src/interfaces/results.ts @@ -37,3 +37,5 @@ export interface Assignment { } export type AssignmentWithCorporateId = Assignment & { corporateId: string }; + +export type AssignmentWithHasResults = Assignment & { hasResults: boolean }; diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index e314b8ab..bfb491b5 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -1,9 +1,9 @@ /* eslint-disable @next/next/no-img-element */ import { Module } from "@/interfaces"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import AbandonPopup from "@/components/AbandonPopup"; -import Layout from "@/components/High/Layout"; +import { LayoutContext } from "@/components/High/Layout"; import Finish from "@/exams/Finish"; import Level from "@/exams/Level"; import Listening from "@/exams/Listening"; @@ -11,9 +11,12 @@ import Reading from "@/exams/Reading"; import Selection from "@/exams/Selection"; import Speaking from "@/exams/Speaking"; import Writing from "@/exams/Writing"; -import { Exam, LevelExam, UserSolution, Variant, WritingExam } from "@/interfaces/exam"; +import { Exam, LevelExam, Variant } from "@/interfaces/exam"; import { User } from "@/interfaces/user"; -import { evaluateSpeakingAnswer, evaluateWritingAnswer } from "@/utils/evaluation"; +import { + evaluateSpeakingAnswer, + evaluateWritingAnswer, +} from "@/utils/evaluation"; import { getExam } from "@/utils/exams"; import axios from "axios"; import { useRouter } from "next/router"; @@ -24,325 +27,435 @@ import useExamStore from "@/stores/exam"; import useEvaluationPolling from "@/hooks/useEvaluationPolling"; interface Props { - page: "exams" | "exercises"; - user: User; - destination?: string - hideSidebar?: boolean + page: "exams" | "exercises"; + user: User; + destination?: string; + hideSidebar?: boolean; } -export default function ExamPage({ page, user, destination = "/", hideSidebar = false }: Props) { - const router = useRouter(); - const [variant, setVariant] = useState("full"); - const [avoidRepeated, setAvoidRepeated] = useState(false); - const [showAbandonPopup, setShowAbandonPopup] = useState(false); - const [moduleLock, setModuleLock] = useState(false); +export default function ExamPage({ + page, + user, + destination = "/", + hideSidebar = false, +}: Props) { + const router = useRouter(); + const [variant, setVariant] = useState("full"); + const [avoidRepeated, setAvoidRepeated] = useState(false); + const [showAbandonPopup, setShowAbandonPopup] = useState(false); + const [moduleLock, setModuleLock] = useState(false); - const { - exam, setExam, - exams, - sessionId, setSessionId, setPartIndex, - moduleIndex, setModuleIndex, - setQuestionIndex, setExerciseIndex, - userSolutions, setUserSolutions, - showSolutions, setShowSolutions, - selectedModules, setSelectedModules, - setUser, - inactivity, - timeSpent, - assignment, - bgColor, - flags, - dispatch, - reset: resetStore, - saveStats, - saveSession, - setFlags, - setShuffles, - } = useExamStore(); + const { + exam, + setExam, + exams, + sessionId, + setSessionId, + setPartIndex, + moduleIndex, + setModuleIndex, + setQuestionIndex, + setExerciseIndex, + userSolutions, + setUserSolutions, + showSolutions, + setShowSolutions, + selectedModules, + setSelectedModules, + setUser, + inactivity, + timeSpent, + assignment, + bgColor, + flags, + dispatch, + reset: resetStore, + saveStats, + saveSession, + setFlags, + setShuffles, + } = useExamStore(); - const [isFetchingExams, setIsFetchingExams] = useState(false); - const [isExamLoaded, setIsExamLoaded] = useState(moduleIndex < selectedModules.length); + const [isFetchingExams, setIsFetchingExams] = useState(false); + const [isExamLoaded, setIsExamLoaded] = useState( + moduleIndex < selectedModules.length + ); - useEffect(() => { - setIsExamLoaded(moduleIndex < selectedModules.length); - }, [showSolutions, moduleIndex, selectedModules]); + useEffect(() => { + setIsExamLoaded(moduleIndex < selectedModules.length); + }, [showSolutions, moduleIndex, selectedModules]); - useEffect(() => { - if (!showSolutions && sessionId.length === 0 && user?.id) { - const shortUID = new ShortUniqueId(); - setUser(user.id); - setSessionId(shortUID.randomUUID(8)); - } - }, [setSessionId, isExamLoaded, sessionId, showSolutions, setUser, user?.id]); + useEffect(() => { + if (!showSolutions && sessionId.length === 0 && user?.id) { + const shortUID = new ShortUniqueId(); + setUser(user.id); + setSessionId(shortUID.randomUUID(8)); + } + }, [setSessionId, isExamLoaded, sessionId, showSolutions, setUser, user?.id]); - useEffect(() => { - if (user?.type === "developer") console.log(exam); - }, [exam, user]); + useEffect(() => { + if (user?.type === "developer") console.log(exam); + }, [exam, user]); - useEffect(() => { - (async () => { - if (selectedModules.length > 0 && exams.length === 0) { - setIsFetchingExams(true); - const examPromises = selectedModules.map((module) => - getExam( - module, - avoidRepeated, - variant, - user?.type === "student" || user?.type === "developer" ? user.preferredGender : undefined, - ), - ); - Promise.all(examPromises).then((values) => { - setIsFetchingExams(false); - if (values.every((x) => !!x)) { - dispatch({ type: 'INIT_EXAM', payload: { exams: values.map((x) => x!), modules: selectedModules } }) - } else { - toast.error("Something went wrong, please try again"); - setTimeout(router.reload, 500); - } - }); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedModules, exams]); + useEffect(() => { + (async () => { + if (selectedModules.length > 0 && exams.length === 0) { + setIsFetchingExams(true); + const examPromises = selectedModules.map((module) => + getExam( + module, + avoidRepeated, + variant, + user?.type === "student" || user?.type === "developer" + ? user.preferredGender + : undefined + ) + ); + Promise.all(examPromises).then((values) => { + setIsFetchingExams(false); + if (values.every((x) => !!x)) { + dispatch({ + type: "INIT_EXAM", + payload: { + exams: values.map((x) => x!), + modules: selectedModules, + }, + }); + } else { + toast.error("Something went wrong, please try again"); + setTimeout(router.reload, 500); + } + }); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedModules, exams]); + const reset = () => { + resetStore(); + setVariant("full"); + setAvoidRepeated(false); + setShowAbandonPopup(false); + }; - const reset = () => { - resetStore(); - setVariant("full"); - setAvoidRepeated(false); - setShowAbandonPopup(false); - }; + useEvaluationPolling(sessionId ? [sessionId] : [], "exam", user?.id); - useEvaluationPolling(sessionId ? [sessionId] : [], "exam", user?.id); + useEffect(() => { + setModuleLock(true); + }, [flags.finalizeModule]); - useEffect(() => { - setModuleLock(true); - }, [flags.finalizeModule]) + useEffect(() => { + if (flags.finalizeModule && !showSolutions) { + if ( + exam && + (exam.module === "writing" || exam.module === "speaking") && + userSolutions.length > 0 + ) { + (async () => { + try { + const results = await Promise.all( + exam.exercises.map(async (exercise, index) => { + if (exercise.type === "writing") { + const sol = await evaluateWritingAnswer( + user.id, + sessionId, + exercise, + index + 1, + userSolutions.find((x) => x.exercise === exercise.id)!, + exercise.attachment?.url + ); + return sol; + } + if ( + exercise.type === "interactiveSpeaking" || + exercise.type === "speaking" + ) { + const sol = await evaluateSpeakingAnswer( + user.id, + sessionId, + exercise, + userSolutions.find((x) => x.exercise === exercise.id)!, + index + 1 + ); + return sol; + } + return null; + }) + ); + const updatedSolutions = userSolutions.map((solution) => { + const completed = results + .filter((r) => r !== null) + .find((c: any) => c.exercise === solution.exercise); + return completed || solution; + }); + setUserSolutions(updatedSolutions); + } catch (error) { + console.error("Error during module evaluation:", error); + } finally { + setModuleLock(false); + } + })(); + } else { + setModuleLock(false); + } + } + }, [ + exam, + showSolutions, + userSolutions, + sessionId, + user.id, + flags.finalizeModule, + setUserSolutions, + ]); - useEffect(() => { - if (flags.finalizeModule && !showSolutions) { - if (exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0) { - (async () => { - try { - const results = await Promise.all( - exam.exercises.map(async (exercise, index) => { - if (exercise.type === "writing") { - const sol = await evaluateWritingAnswer( - user.id, sessionId, exercise, index + 1, - userSolutions.find((x) => x.exercise === exercise.id)!, - exercise.attachment?.url - ); - return sol; - } - if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") { - const sol = await evaluateSpeakingAnswer( - user.id, - sessionId, - exercise, - userSolutions.find((x) => x.exercise === exercise.id)!, - index + 1, - ); - return sol; - } - return null; - }) - ); - const updatedSolutions = userSolutions.map(solution => { - const completed = results.filter(r => r !== null).find( - (c: any) => c.exercise === solution.exercise - ); - return completed || solution; - }); - setUserSolutions(updatedSolutions); - } catch (error) { - console.error('Error during module evaluation:', error); - } finally { - setModuleLock(false); - } - })(); - } else { - setModuleLock(false); - } - } - }, [exam, showSolutions, userSolutions, sessionId, user.id, flags.finalizeModule, setUserSolutions]); + useEffect(() => { + if (flags.finalizeExam && moduleIndex !== -1 && !moduleLock) { + (async () => { + setModuleIndex(-1); + await saveStats(); + await axios.get("/api/stats/update"); + })(); + } + }, [ + flags.finalizeExam, + moduleIndex, + saveStats, + setModuleIndex, + userSolutions, + moduleLock, + flags.finalizeModule, + ]); - useEffect(() => { - if (flags.finalizeExam && moduleIndex !== -1 && !moduleLock) { - (async () => { - setModuleIndex(-1); - await saveStats(); - await axios.get("/api/stats/update"); - })() - } - }, [flags.finalizeExam, moduleIndex, saveStats, setModuleIndex, userSolutions, moduleLock, flags.finalizeModule]); + useEffect(() => { + if ( + flags.finalizeExam && + !userSolutions.some((s) => s.isDisabled) && + !moduleLock + ) { + setShowSolutions(true); + setFlags({ finalizeExam: false }); + dispatch({ type: "UPDATE_EXAMS" }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flags.finalizeExam, userSolutions, showSolutions, moduleLock]); - useEffect(() => { - if (flags.finalizeExam && !userSolutions.some(s => s.isDisabled) && !moduleLock) { - setShowSolutions(true); - setFlags({ finalizeExam: false }); - dispatch({ type: "UPDATE_EXAMS" }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [flags.finalizeExam, userSolutions, showSolutions, moduleLock]); + const aggregateScoresByModule = ( + isPractice?: boolean + ): { + module: Module; + total: number; + missing: number; + correct: number; + }[] => { + const scores: { + [key in Module]: { total: number; missing: number; correct: number }; + } = { + reading: { + total: 0, + correct: 0, + missing: 0, + }, + listening: { + total: 0, + correct: 0, + missing: 0, + }, + writing: { + total: 0, + correct: 0, + missing: 0, + }, + speaking: { + total: 0, + correct: 0, + missing: 0, + }, + level: { + total: 0, + correct: 0, + missing: 0, + }, + }; + userSolutions.forEach((x) => { + if (x.isPractice === isPractice) { + const examModule = + x.module || + (x.type === "writing" + ? "writing" + : x.type === "speaking" || x.type === "interactiveSpeaking" + ? "speaking" + : undefined); - const aggregateScoresByModule = (isPractice?: boolean): { - module: Module; - total: number; - missing: number; - correct: number; - }[] => { - const scores: { - [key in Module]: { total: number; missing: number; correct: number }; - } = { - reading: { - total: 0, - correct: 0, - missing: 0, - }, - listening: { - total: 0, - correct: 0, - missing: 0, - }, - writing: { - total: 0, - correct: 0, - missing: 0, - }, - speaking: { - total: 0, - correct: 0, - missing: 0, - }, - level: { - total: 0, - correct: 0, - missing: 0, - }, - }; + scores[examModule!] = { + total: scores[examModule!].total + x.score.total, + correct: scores[examModule!].correct + x.score.correct, + missing: scores[examModule!].missing + x.score.missing, + }; + } + }); - userSolutions.filter(x => isPractice ? x.isPractice : !x.isPractice).forEach((x) => { - const examModule = - x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined); + return Object.keys(scores).reduce< + { module: Module; total: number; missing: number; correct: number }[] + >((accm, x) => { + if (scores[x as Module].total > 0) + accm.push({ module: x as Module, ...scores[x as Module] }); + return accm; + }, []); + }; - scores[examModule!] = { - total: scores[examModule!].total + x.score.total, - correct: scores[examModule!].correct + x.score.correct, - missing: scores[examModule!].missing + x.score.missing, - }; - }); + const ModuleExamMap: Record>> = { + reading: Reading as React.ComponentType>, + listening: Listening as React.ComponentType>, + writing: Writing as React.ComponentType>, + speaking: Speaking as React.ComponentType>, + level: Level as React.ComponentType>, + }; - return Object.keys(scores) - .filter((x) => scores[x as Module].total > 0) - .map((x) => ({ module: x as Module, ...scores[x as Module] })); - }; + const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined; - const ModuleExamMap: Record>> = { - "reading": Reading as React.ComponentType>, - "listening": Listening as React.ComponentType>, - "writing": Writing as React.ComponentType>, - "speaking": Speaking as React.ComponentType>, - "level": Level as React.ComponentType>, - } + const onAbandon = async () => { + await saveSession(); + reset(); + }; - const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined; + const { + setBgColor, + setHideSidebar, + setFocusMode, + setOnFocusLayerMouseEnter, + } = React.useContext(LayoutContext); - const onAbandon = async () => { - await saveSession(); - reset(); - }; + useEffect(() => { + setOnFocusLayerMouseEnter(() => () => setShowAbandonPopup(true)); + }, []); - return ( - <> - - {user && ( - setShowAbandonPopup(true)}> - <> - {/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/} - {selectedModules.length === 0 && { - setModuleIndex(0); - setAvoidRepeated(avoid); - setSelectedModules(modules); - setVariant(variant); - }} - />} - {isFetchingExams && ( -
- - Loading Exam ... -
- )} - {(moduleIndex === -1 && selectedModules.length !== 0) && - s.isDisabled)} - user={user!} - modules={selectedModules} - solutions={userSolutions} - assignment={assignment} - information={{ - timeSpent, - inactivity, - }} - destination={destination} - onViewResults={(index?: number) => { - if (exams[0].module === "level") { - const levelExam = exams[0] as LevelExam; - const allExercises = levelExam.parts.flatMap((part) => part.exercises); - const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index])); - const orderedSolutions = userSolutions.slice().sort((a, b) => { - const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity; - const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity; - return indexA - indexB; - }); - setUserSolutions(orderedSolutions); - } else { - setUserSolutions(userSolutions); - } - setShuffles([]); - if (index === undefined) { - setFlags({ reviewAll: true }); - setModuleIndex(0); - setExam(exams[0]); - } else { - setModuleIndex(index); - setExam(exams[index]); - } - setShowSolutions(true); - setQuestionIndex(0); - setExerciseIndex(0); - setPartIndex(0); - }} - scores={aggregateScoresByModule()} - practiceScores={aggregateScoresByModule(true)} - />} - {/* Exam is on going, display it and the abandon modal */} - {isExamLoaded && moduleIndex !== -1 && ( - <> - {exam && CurrentExam && } - {!showSolutions && setShowAbandonPopup(false)} - /> - } - - )} - -
- )} - - ); + useEffect(() => { + setBgColor(bgColor); + setHideSidebar(hideSidebar); + setFocusMode( + selectedModules.length !== 0 && + !showSolutions && + moduleIndex < selectedModules.length + ); + }, [ + bgColor, + hideSidebar, + moduleIndex, + selectedModules.length, + setBgColor, + setFocusMode, + setHideSidebar, + showSolutions, + ]); + + return ( + <> + + {user && ( + <> + {/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/} + {selectedModules.length === 0 && ( + { + setModuleIndex(0); + setAvoidRepeated(avoid); + setSelectedModules(modules); + setVariant(variant); + }} + /> + )} + {isFetchingExams && ( +
+ + + Loading Exam ... + +
+ )} + {moduleIndex === -1 && selectedModules.length !== 0 && ( + s.isDisabled)} + user={user!} + modules={selectedModules} + solutions={userSolutions} + assignment={assignment} + information={{ + timeSpent, + inactivity, + }} + destination={destination} + onViewResults={(index?: number) => { + if (exams[0].module === "level") { + const levelExam = exams[0] as LevelExam; + const allExercises = levelExam.parts.flatMap( + (part) => part.exercises + ); + const exerciseOrderMap = new Map( + allExercises.map((ex, index) => [ex.id, index]) + ); + const orderedSolutions = userSolutions + .slice() + .sort((a, b) => { + const indexA = + exerciseOrderMap.get(a.exercise) ?? Infinity; + const indexB = + exerciseOrderMap.get(b.exercise) ?? Infinity; + return indexA - indexB; + }); + setUserSolutions(orderedSolutions); + } else { + setUserSolutions(userSolutions); + } + setShuffles([]); + if (index === undefined) { + setFlags({ reviewAll: true }); + setModuleIndex(0); + setExam(exams[0]); + } else { + setModuleIndex(index); + setExam(exams[index]); + } + setShowSolutions(true); + setQuestionIndex(0); + setExerciseIndex(0); + setPartIndex(0); + }} + scores={aggregateScoresByModule()} + practiceScores={aggregateScoresByModule(true)} + /> + )} + {/* Exam is on going, display it and the abandon modal */} + {isExamLoaded && moduleIndex !== -1 && ( + <> + {exam && CurrentExam && ( + + )} + {!showSolutions && ( + setShowAbandonPopup(false)} + /> + )} + + )} + + )} + + ); } diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 5dfc9842..7ff8b626 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -1,18 +1,14 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; -import useGroups from "@/hooks/useGroups"; -import usePackages from "@/hooks/usePackages"; import useUsers from "@/hooks/useUsers"; import { User } from "@/interfaces/user"; import clsx from "clsx"; -import { capitalize, sortBy } from "lodash"; +import { capitalize } from "lodash"; import { useEffect, useMemo, useState } from "react"; import useInvites from "@/hooks/useInvites"; import { BsArrowRepeat } from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; import { useRouter } from "next/router"; import { ToastContainer } from "react-toastify"; -import useDiscounts from "@/hooks/useDiscounts"; import PaymobPayment from "@/components/PaymobPayment"; import moment from "moment"; import { EntityWithRoles } from "@/interfaces/entity"; @@ -22,241 +18,345 @@ import { useAllowedEntities } from "@/hooks/useEntityPermissions"; import Select from "@/components/Low/Select"; interface Props { - user: User - discounts: Discount[] - packages: Package[] - entities: EntityWithRoles[] - hasExpired?: boolean; - reload: () => void; + user: User; + discounts: Discount[]; + packages: Package[]; + entities: EntityWithRoles[]; + hasExpired?: boolean; + reload: () => void; } -export default function PaymentDue({ user, discounts = [], entities = [], packages = [], hasExpired = false, reload }: Props) { - const [isLoading, setIsLoading] = useState(false); - const [entity, setEntity] = useState() +export default function PaymentDue({ + user, + discounts = [], + entities = [], + packages = [], + hasExpired = false, + reload, +}: Props) { + const [isLoading, setIsLoading] = useState(false); + const [entity, setEntity] = useState(); - const router = useRouter(); + const router = useRouter(); - const { users } = useUsers(); - const { invites, isLoading: isInvitesLoading, reload: reloadInvites } = useInvites({ to: user?.id }); + const { users } = useUsers(); + const { + invites, + isLoading: isInvitesLoading, + reload: reloadInvites, + } = useInvites({ to: user?.id }); - const isIndividual = useMemo(() => { - if (isAdmin(user)) return false; - if (user?.type !== "student") return false; + const isIndividual = useMemo(() => { + if (isAdmin(user)) return false; + if (user?.type !== "student") return false; - return user.entities.length === 0 - }, [user]) + return user.entities.length === 0; + }, [user]); - const appliedDiscount = useMemo(() => { - const biggestDiscount = [...discounts].sort((a, b) => b.percentage - a.percentage).shift(); + const appliedDiscount = useMemo(() => { + const biggestDiscount = [...discounts] + .sort((a, b) => b.percentage - a.percentage) + .shift(); - if (!biggestDiscount || (biggestDiscount.validUntil && moment(biggestDiscount.validUntil).isBefore(moment()))) - return 0; + if ( + !biggestDiscount || + (biggestDiscount.validUntil && + moment(biggestDiscount.validUntil).isBefore(moment())) + ) + return 0; - return biggestDiscount.percentage - }, [discounts]) + return biggestDiscount.percentage; + }, [discounts]); - const entitiesThatCanBePaid = useAllowedEntities(user, entities, 'pay_entity') + const entitiesThatCanBePaid = useAllowedEntities( + user, + entities, + "pay_entity" + ); - useEffect(() => { - if (entitiesThatCanBePaid.length > 0) setEntity(entitiesThatCanBePaid[0]) - }, [entitiesThatCanBePaid]) + useEffect(() => { + if (entitiesThatCanBePaid.length > 0) setEntity(entitiesThatCanBePaid[0]); + }, [entitiesThatCanBePaid]); - return ( - <> - - {isLoading && ( -
-
- - Completing your payment... - If you canceled your payment or it failed, please click the button below to restart - -
-
- )} - - {invites.length > 0 && ( -
-
-
- Invites - -
-
- - {invites.map((invite) => ( - { - reloadInvites(); - router.reload(); - }} - /> - ))} - -
- )} + return ( + <> + + {isLoading && ( +
+
+ + + Completing your payment... + + + If you canceled your payment or it failed, please click the button + below to restart + + +
+
+ )} + <> + {invites.length > 0 && ( +
+
+
+ + Invites + + +
+
+ + {invites.map((invite) => ( + { + reloadInvites(); + router.reload(); + }} + /> + ))} + +
+ )} -
- {hasExpired && You do not have time credits for your account type!} - {isIndividual && ( -
- - To add to your use of EnCoach, please purchase one of the time packages available below: - -
- {packages.map((p) => ( -
-
- EnCoach's Logo - - EnCoach - {p.duration}{" "} - {capitalize( - p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, - )} - -
-
- {appliedDiscount === 0 && ( - - {p.price} {p.currency} - - )} - {appliedDiscount > 0 && ( -
- - {p.price} {p.currency} - - - {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency} - -
- )} - { - setTimeout(reload, 500); - }} - currency={p.currency} - duration={p.duration} - duration_unit={p.duration_unit} - price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} - /> -
-
- This includes: -
    -
  • - Train your abilities for the IELTS exam
  • -
  • - Gain insights into your weaknesses and strengths
  • -
  • - Allow yourself to correctly prepare for the exam
  • -
-
-
- ))} -
-
- )} +
+ {hasExpired && ( + + You do not have time credits for your account type! + + )} + {isIndividual && ( +
+ + To add to your use of EnCoach, please purchase one of the time + packages available below: + +
+ {packages.map((p) => ( +
+
+ EnCoach's Logo + + EnCoach - {p.duration}{" "} + {capitalize( + p.duration === 1 + ? p.duration_unit.slice( + 0, + p.duration_unit.length - 1 + ) + : p.duration_unit + )} + +
+
+ {appliedDiscount === 0 && ( + + {p.price} {p.currency} + + )} + {appliedDiscount > 0 && ( +
+ + {p.price} {p.currency} + + + {( + p.price - + p.price * (appliedDiscount / 100) + ).toFixed(2)}{" "} + {p.currency} + +
+ )} + { + setTimeout(reload, 500); + }} + currency={p.currency} + duration={p.duration} + duration_unit={p.duration_unit} + price={ + +( + p.price - + p.price * (appliedDiscount / 100) + ).toFixed(2) + } + /> +
+
+ This includes: +
    +
  • - Train your abilities for the IELTS exam
  • +
  • + - Gain insights into your weaknesses and strengths +
  • +
  • + - Allow yourself to correctly prepare for the exam +
  • +
+
+
+ ))} +
+
+ )} - {!isIndividual && entitiesThatCanBePaid.length > 0 && - entity?.payment && ( -
-
- - ({ + value: e.id, + label: e.label, + entity: e, + }))} + onChange={(e) => (e?.value ? setEntity(e?.entity) : null)} + className="!w-full max-w-[400px] self-center" + /> +
- - To add to your use of EnCoach and that of your students and teachers, please pay your designated package - below: - -
-
- EnCoach's Logo - - EnCoach - {12} Months - -
-
- - {entity.payment.price} {entity.payment.currency} - - { - setIsLoading(false); - setTimeout(reload, 500); - }} - /> -
-
- This includes: -
    -
  • - - Allow a total of {entity.licenses} students and teachers to use EnCoach -
  • -
  • - Train their abilities for the IELTS exam
  • -
  • - Gain insights into your students' weaknesses and strengths
  • -
  • - Allow them to correctly prepare for the exam
  • -
-
-
-
- )} - {!isIndividual && entitiesThatCanBePaid.length === 0 && ( -
- - You are not the person in charge of your time credits, please contact your administrator about this situation. - - - If you believe this to be a mistake, please contact the platform's administration, thank you for your - patience. - -
- )} - {!isIndividual && - entitiesThatCanBePaid.length > 0 && - !entity?.payment && ( -
-
- - ({ + value: e.id, + label: e.label, + entity: e, + }))} + onChange={(e) => (e?.value ? setEntity(e?.entity) : null)} + className="!w-full max-w-[400px] self-center" + /> +
+ + An admin nor your agent have yet set the price intended to + your requirements in terms of the amount of users you desire + and your expected monthly duration. + + + Please try again later or contact your agent or an admin, + thank you for your patience. + +
+ )} +
+ + + ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b12f32a5..fada3931 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,35 +1,71 @@ import "@/styles/globals.css"; import "react-toastify/dist/ReactToastify.css"; -import type {AppProps} from "next/app"; +import type { AppProps } from "next/app"; import "primereact/resources/themes/lara-light-indigo/theme.css"; import "primereact/resources/primereact.min.css"; import "primeicons/primeicons.css"; import "react-datepicker/dist/react-datepicker.css"; -import {useRouter} from "next/router"; -import {useEffect} from "react"; +import { Router, useRouter } from "next/router"; +import { useEffect, useState } from "react"; import useExamStore from "@/stores/exam"; import usePreferencesStore from "@/stores/preferencesStore"; +import Layout from "../components/High/Layout"; +import useEntities from "../hooks/useEntities"; +import UserProfileSkeleton from "../components/Medium/UserProfileSkeleton"; -export default function App({Component, pageProps}: AppProps) { - const {reset} = useExamStore(); - const setIsSidebarMinimized = usePreferencesStore((state) => state.setSidebarMinimized); +export default function App({ Component, pageProps }: AppProps) { + const [loading, setLoading] = useState(false); - const router = useRouter(); + const { reset } = useExamStore(); - useEffect(() => { - if (router.pathname !== "/exam" && router.pathname !== "/exercises") reset(); - }, [router.pathname, reset]); + const setIsSidebarMinimized = usePreferencesStore( + (state) => state.setSidebarMinimized + ); - useEffect(() => { - if (localStorage.getItem("isSidebarMinimized")) { - if (localStorage.getItem("isSidebarMinimized") === "true") { - setIsSidebarMinimized(true); - } else { - setIsSidebarMinimized(false); - } - } - }, [setIsSidebarMinimized]); + const router = useRouter(); - return ; + const { entities } = useEntities(!pageProps?.user?.id); + + useEffect(() => { + const start = () => { + setLoading(true); + }; + const end = () => { + setLoading(false); + }; + Router.events.on("routeChangeStart", start); + Router.events.on("routeChangeComplete", end); + Router.events.on("routeChangeError", end); + return () => { + Router.events.off("routeChangeStart", start); + Router.events.off("routeChangeComplete", end); + Router.events.off("routeChangeError", end); + }; + }, []); + + useEffect(() => { + if (router.pathname !== "/exam" && router.pathname !== "/exercises") + reset(); + }, [router.pathname, reset]); + + useEffect(() => { + if (localStorage.getItem("isSidebarMinimized")) { + if (localStorage.getItem("isSidebarMinimized") === "true") { + setIsSidebarMinimized(true); + } else { + setIsSidebarMinimized(false); + } + } + }, [setIsSidebarMinimized]); + + return ( + + {loading ? ( + + ) : ( + + )} + + ); } diff --git a/src/pages/api/stats/user/[user].ts b/src/pages/api/stats/user/[user].ts index 35aa9bf2..3bc33e74 100644 --- a/src/pages/api/stats/user/[user].ts +++ b/src/pages/api/stats/user/[user].ts @@ -1,24 +1,20 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from "next"; -import client from "@/lib/mongodb"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { getDetailedStatsByUser } from "../../../../utils/stats.be"; -const db = client.db(process.env.MONGODB_DB); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) { - res.status(401).json({ok: false}); + res.status(401).json({ ok: false }); return; } - const {user} = req.query; - const snapshot = await db.collection("stats").aggregate([ - { $match: { user: user } }, - { $sort: { "date": 1 } } - ]).toArray(); + const { user, query } = req.query as { user: string, query?: string }; + const snapshot = await getDetailedStatsByUser(user, query); res.status(200).json(snapshot); } \ No newline at end of file diff --git a/src/pages/assignments/[id].tsx b/src/pages/assignments/[id].tsx index 51ae2a62..6a806362 100644 --- a/src/pages/assignments/[id].tsx +++ b/src/pages/assignments/[id].tsx @@ -1,15 +1,12 @@ import Button from "@/components/Low/Button"; import ProgressBar from "@/components/Low/ProgressBar"; -import Modal from "@/components/Modal"; -import useUsers from "@/hooks/useUsers"; import { Grading, Module } from "@/interfaces"; import { Assignment } from "@/interfaces/results"; -import { Group, Stat, User } from "@/interfaces/user"; +import { Stat, User } from "@/interfaces/user"; import useExamStore from "@/stores/exam"; import { getExamById } from "@/utils/exams"; import { sortByModule } from "@/utils/moduleUtils"; import { calculateBandScore, getGradingLabel } from "@/utils/score"; -import { convertToUserSolutions } from "@/utils/stats"; import { getUserName } from "@/utils/users"; import axios from "axios"; import clsx from "clsx"; @@ -23,13 +20,11 @@ import { withIronSessionSsr } from "iron-session/next"; import { checkAccess, doesEntityAllow } from "@/utils/permissions"; import { mapBy, redirect, serialize } from "@/utils"; import { getAssignment } from "@/utils/assignments.be"; -import { getEntitiesUsers, getEntityUsers, getUsers } from "@/utils/users.be"; -import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be"; -import { getGroups, getGroupsByEntities, getGroupsByEntity } from "@/utils/groups.be"; +import { getEntityUsers, getUsers } from "@/utils/users.be"; +import { getEntityWithRoles } from "@/utils/entities.be"; import { sessionOptions } from "@/lib/session"; import { EntityWithRoles } from "@/interfaces/entity"; import Head from "next/head"; -import Layout from "@/components/High/Layout"; import Separator from "@/components/Low/Separator"; import Link from "next/link"; import { requestUser } from "@/utils/api"; @@ -353,7 +348,7 @@ export default function AssignmentView({ user, users, entity, assignment, gradin - + <>
@@ -466,7 +461,7 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
- + ); } diff --git a/src/pages/assignments/creator/[id].tsx b/src/pages/assignments/creator/[id].tsx index 4d6206ea..5c9d18bb 100644 --- a/src/pages/assignments/creator/[id].tsx +++ b/src/pages/assignments/creator/[id].tsx @@ -1,4 +1,3 @@ -import Layout from "@/components/High/Layout"; import Button from "@/components/Low/Button"; import Checkbox from "@/components/Low/Checkbox"; import Input from "@/components/Low/Input"; @@ -212,7 +211,7 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro - + <>
@@ -589,7 +588,7 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
-
+ ); } diff --git a/src/pages/assignments/creator/index.tsx b/src/pages/assignments/creator/index.tsx index 912a99b6..c2771860 100644 --- a/src/pages/assignments/creator/index.tsx +++ b/src/pages/assignments/creator/index.tsx @@ -1,4 +1,3 @@ -import Layout from "@/components/High/Layout"; import Button from "@/components/Low/Button"; import Checkbox from "@/components/Low/Checkbox"; import Input from "@/components/Low/Input"; @@ -170,7 +169,7 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props - + <>
@@ -528,7 +527,7 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
-
+ ); } diff --git a/src/pages/assignments/index.tsx b/src/pages/assignments/index.tsx index ae0c1eca..113de22f 100644 --- a/src/pages/assignments/index.tsx +++ b/src/pages/assignments/index.tsx @@ -1,15 +1,12 @@ -import Layout from "@/components/High/Layout"; import Separator from "@/components/Low/Separator"; import AssignmentCard from "@/components/AssignmentCard"; -import AssignmentView from "@/components/AssignmentView"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; import { useListSearch } from "@/hooks/useListSearch"; import usePagination from "@/hooks/usePagination"; import { EntityWithRoles } from "@/interfaces/entity"; import { Assignment } from "@/interfaces/results"; -import { CorporateUser, Group, User } from "@/interfaces/user"; +import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { getUserCompanyName } from "@/resources/user"; import { findBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { @@ -21,15 +18,13 @@ import { } from "@/utils/assignments"; import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; -import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; import { checkAccess, findAllowedEntities } from "@/utils/permissions"; import { getEntitiesUsers, getUsers } from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; -import { groupBy } from "lodash"; import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { BsChevronLeft, BsPlus } from "react-icons/bs"; export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { @@ -98,7 +93,7 @@ export default function AssignmentsPage({ assignments, entities, user, users }: - + <>
@@ -223,7 +218,7 @@ export default function AssignmentsPage({ assignments, entities, user, users }: ))}
- + ); } diff --git a/src/pages/classrooms/[id].tsx b/src/pages/classrooms/[id].tsx index 38babb7a..c6eb70ce 100644 --- a/src/pages/classrooms/[id].tsx +++ b/src/pages/classrooms/[id].tsx @@ -1,5 +1,4 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import Tooltip from "@/components/Low/Tooltip"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; import { useListSearch } from "@/hooks/useListSearch"; @@ -183,7 +182,7 @@ export default function Home({ user, group, users, entity }: Props) { {user && ( - + <>
@@ -339,7 +338,7 @@ export default function Home({ user, group, users, entity }: Props) { ))}
-
+ )} ); diff --git a/src/pages/classrooms/create.tsx b/src/pages/classrooms/create.tsx index b0192c08..be248ae7 100644 --- a/src/pages/classrooms/create.tsx +++ b/src/pages/classrooms/create.tsx @@ -1,19 +1,18 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import Tooltip from "@/components/Low/Tooltip"; import {useListSearch} from "@/hooks/useListSearch"; import usePagination from "@/hooks/usePagination"; -import {Entity, EntityWithRoles} from "@/interfaces/entity"; +import {EntityWithRoles} from "@/interfaces/entity"; import {User} from "@/interfaces/user"; import {sessionOptions} from "@/lib/session"; import {USER_TYPE_LABELS} from "@/resources/user"; import {filterBy, mapBy, redirect, serialize} from "@/utils"; -import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; +import { getEntitiesWithRoles} from "@/utils/entities.be"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import {getUserName, isAdmin} from "@/utils/users"; -import {getEntitiesUsers, getLinkedUsers} from "@/utils/users.be"; +import {getEntitiesUsers} from "@/utils/users.be"; import axios from "axios"; import clsx from "clsx"; import {withIronSessionSsr} from "iron-session/next"; @@ -102,7 +101,7 @@ export default function Home({user, users, entities}: Props) { - + <>
@@ -217,7 +216,7 @@ export default function Home({user, users, entities}: Props) { ))}
-
+ ); } diff --git a/src/pages/classrooms/index.tsx b/src/pages/classrooms/index.tsx index 293ac3d1..d18baac5 100644 --- a/src/pages/classrooms/index.tsx +++ b/src/pages/classrooms/index.tsx @@ -3,7 +3,6 @@ import Head from "next/head"; import { withIronSessionSsr } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; import { ToastContainer } from "react-toastify"; -import Layout from "@/components/High/Layout"; import { GroupWithUsers, User } from "@/interfaces/user"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { getUserName, isAdmin } from "@/utils/users"; @@ -11,13 +10,13 @@ import { convertToUsers, getGroupsForEntities } from "@/utils/groups.be"; import { getSpecificUsers } from "@/utils/users.be"; import Link from "next/link"; import { uniq } from "lodash"; -import { BsFillMortarboardFill, BsPlus } from "react-icons/bs"; +import { BsPlus } from "react-icons/bs"; import CardList from "@/components/High/CardList"; import Separator from "@/components/Low/Separator"; import { findBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { findAllowedEntities } from "@/utils/permissions"; -import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; import { EntityWithRoles } from "@/interfaces/entity"; import { FaPersonChalkboard } from "react-icons/fa6"; @@ -122,7 +121,7 @@ export default function Home({ user, groups, entities }: Props) { - + <>
setShowImport(false)} maxWidth="max-w-[85%]"> setShowImport(false)} /> @@ -153,7 +152,7 @@ export default function Home({ user, groups, entities }: Props) { firstCard={entitiesAllowCreate.length === 0 ? undefined : firstCard} />
-
+ ); } diff --git a/src/pages/dashboard/admin.tsx b/src/pages/dashboard/admin.tsx index ab5e7931..05783216 100644 --- a/src/pages/dashboard/admin.tsx +++ b/src/pages/dashboard/admin.tsx @@ -1,191 +1,220 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; import { EntityWithRoles } from "@/interfaces/entity"; -import { Assignment } from "@/interfaces/results"; -import { Group, Stat, Type, User } from "@/interfaces/user"; +import { Stat, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; +import { filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { countEntitiesAssignments, getAssignments } from "@/utils/assignments.be"; -import { getEntitiesWithRoles } from "@/utils/entities.be"; -import { countGroups, getGroups } from "@/utils/groups.be"; +import { + countEntitiesAssignments, +} from "@/utils/assignments.be"; +import { getEntities } from "@/utils/entities.be"; +import { countGroups } from "@/utils/groups.be"; import { checkAccess } from "@/utils/permissions"; -import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; import { groupByExam } from "@/utils/stats"; import { getStatsByUsers } from "@/utils/stats.be"; -import { countUsers, getUser, getUsers } from "@/utils/users.be"; +import { + countUsersByTypes, + getUsers, +} from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; -import { uniqBy } from "lodash"; import Head from "next/head"; import { useRouter } from "next/router"; -import { useMemo } from "react"; import { - BsBank, - BsClipboard2Data, - BsEnvelopePaper, - BsPencilSquare, - BsPeople, - BsPeopleFill, - BsPersonFill, - BsPersonFillGear, + BsBank, + BsEnvelopePaper, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; interface Props { - user: User; - students: User[]; - latestStudents: User[] - latestTeachers: User[] - entities: EntityWithRoles[]; - usersCount: { [key in Type]: number } - assignmentsCount: number; - stats: Stat[]; - groupsCount: number; + user: User; + students: User[]; + latestStudents: User[]; + latestTeachers: User[]; + entities: EntityWithRoles[]; + usersCount: { [key in Type]: number }; + assignmentsCount: number; + stats: Stat[]; + groupsCount: number; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user || !user.isVerified) return redirect("/login") + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); - if (!checkAccess(user, ["admin", "developer"])) return redirect("/") + if (!checkAccess(user, ["admin", "developer"])) return redirect("/"); - const students = await getUsers({ type: 'student' }); - const usersCount = { - student: await countUsers({ type: "student" }), - teacher: await countUsers({ type: "teacher" }), - corporate: await countUsers({ type: "corporate" }), - mastercorporate: await countUsers({ type: "mastercorporate" }), - } + const students = await getUsers( + { type: "student" }, + 10, + { + averageLevel: -1, + }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); - const latestStudents = await getUsers({ type: 'student' }, 10, { registrationDate: -1 }) - const latestTeachers = await getUsers({ type: 'teacher' }, 10, { registrationDate: -1 }) + const usersCount = await countUsersByTypes([ + "student", + "teacher", + "corporate", + "mastercorporate", + ]); - const entities = await getEntitiesWithRoles(); - const assignmentsCount = await countEntitiesAssignments(mapBy(entities, 'id'), { archived: { $ne: true } }); - const groupsCount = await countGroups(); + const latestStudents = await getUsers( + { type: "student" }, + 10, + { + registrationDate: -1, + }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); + const latestTeachers = await getUsers( + { type: "teacher" }, + 10, + { + registrationDate: -1, + }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); - const stats = await getStatsByUsers(mapBy(students, 'id')); + const entities = await getEntities(undefined, { _id: 0, id: 1, label: 1 }); - return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, stats, groupsCount }) }; + const assignmentsCount = await countEntitiesAssignments( + mapBy(entities, "id"), + { archived: { $ne: true } } + ); + + const groupsCount = await countGroups(); + + const stats = await getStatsByUsers(mapBy(students, "id")); + + return { + props: serialize({ + user, + students, + latestStudents, + latestTeachers, + usersCount, + entities, + assignmentsCount, + stats, + groupsCount, + }), + }; }, sessionOptions); export default function Dashboard({ - user, - students, - latestStudents, - latestTeachers, - usersCount, - entities, - assignmentsCount, - stats, - groupsCount + user, + students, + latestStudents, + latestTeachers, + usersCount, + entities, + assignmentsCount, + stats, + groupsCount, }: Props) { - const router = useRouter(); + const router = useRouter(); - return ( - <> - - EnCoach - - - - - - -
- router.push("/users?type=student")} - Icon={BsPersonFill} - label="Students" - value={usersCount.student} - color="purple" - /> - router.push("/users?type=teacher")} - Icon={BsPencilSquare} - label="Teachers" - value={usersCount.teacher} - color="purple" - /> - router.push("/users?type=corporate")} - label="Corporates" - value={usersCount.corporate} - color="purple" - /> - router.push("/users?type=mastercorporate")} - label="Master Corporates" - value={usersCount.mastercorporate} - color="purple" - /> - router.push("/classrooms")} - label="Classrooms" - value={groupsCount} - color="purple" - /> - router.push("/entities")} - label="Entities" - value={entities.length} - color="purple" - /> - router.push("/statistical")} - label="Entity Statistics" - value={entities.length} - color="purple" - /> - router.push("/users/performance")} - label="Student Performance" - value={usersCount.student} - color="purple" - /> - router.push("/assignments")} - label="Assignments" - value={assignmentsCount} - color="purple" - /> -
+ return ( + <> + + EnCoach + + + + + + <> +
+ router.push("/users?type=student")} + Icon={BsPersonFill} + label="Students" + value={usersCount.student} + color="purple" + /> + router.push("/users?type=teacher")} + Icon={BsPencilSquare} + label="Teachers" + value={usersCount.teacher} + color="purple" + /> + router.push("/users?type=corporate")} + label="Corporates" + value={usersCount.corporate} + color="purple" + /> + router.push("/users?type=mastercorporate")} + label="Master Corporates" + value={usersCount.mastercorporate} + color="purple" + /> + router.push("/classrooms")} + label="Classrooms" + value={groupsCount} + color="purple" + /> + router.push("/entities")} + label="Entities" + value={entities.length} + color="purple" + /> + router.push("/statistical")} + label="Entity Statistics" + value={entities.length} + color="purple" + /> + router.push("/users/performance")} + label="Student Performance" + value={usersCount.student} + color="purple" + /> + router.push("/assignments")} + label="Assignments" + value={assignmentsCount} + color="purple" + /> +
-
- - - calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} - title="Highest level students" - /> - - Object.keys(groupByExam(filterBy(stats, "user", b))).length - - Object.keys(groupByExam(filterBy(stats, "user", a))).length, - ) - } - title="Highest exam count students" - /> -
-
- - ); +
+ + + + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length + )} + title="Highest exam count students" + /> +
+ + + ); } diff --git a/src/pages/dashboard/corporate.tsx b/src/pages/dashboard/corporate.tsx index 3eb71ca5..5abc3604 100644 --- a/src/pages/dashboard/corporate.tsx +++ b/src/pages/dashboard/corporate.tsx @@ -1,194 +1,265 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; import { EntityWithRoles } from "@/interfaces/entity"; import { Stat, StudentUser, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; +import { filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { countEntitiesAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; import { countGroupsByEntities } from "@/utils/groups.be"; -import { checkAccess, findAllowedEntities } from "@/utils/permissions"; -import { calculateAverageLevel } from "@/utils/score"; +import { + checkAccess, + groupAllowedEntitiesByPermissions, +} from "@/utils/permissions"; import { groupByExam } from "@/utils/stats"; -import { getStatsByUsers } from "@/utils/stats.be"; -import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be"; +import { + countAllowedUsers, + getUsers, +} from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; -import { uniqBy } from "lodash"; import moment from "moment"; import Head from "next/head"; import { useRouter } from "next/router"; import { useMemo } from "react"; import { - BsClipboard2Data, - BsClock, - BsEnvelopePaper, - BsPencilSquare, - BsPeople, - BsPeopleFill, - BsPersonFill, - BsPersonFillGear, + BsClock, + BsEnvelopePaper, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; import { isAdmin } from "@/utils/users"; interface Props { - user: User; - students: StudentUser[] - latestStudents: User[] - latestTeachers: User[] - userCounts: { [key in Type]: number } - entities: EntityWithRoles[]; - assignmentsCount: number; - stats: Stat[]; - groupsCount: number; + user: User; + students: StudentUser[]; + latestStudents: User[]; + latestTeachers: User[]; + userCounts: { [key in Type]: number }; + entities: EntityWithRoles[]; + assignmentsCount: number; + stats: Stat[]; + groupsCount: number; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user || !user.isVerified) return redirect("/login") + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); - if (!checkAccess(user, ["admin", "developer", "corporate"])) return redirect("/") + if (!checkAccess(user, ["admin", "developer", "corporate"])) + return redirect("/"); - const entityIDS = mapBy(user.entities, "id") || []; - const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); + const entityIDS = mapBy(user.entities, "id") || []; + const entities = await getEntitiesWithRoles( + isAdmin(user) ? undefined : entityIDS + ); - const allowedStudentEntities = findAllowedEntities(user, entities, "view_students") - const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers") + const { + ["view_students"]: allowedStudentEntities, + ["view_teachers"]: allowedTeacherEntities, + } = groupAllowedEntitiesByPermissions(user, entities, [ + "view_students", + "view_teachers", + ]); - const students = - await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 }); - const latestStudents = - await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { registrationDate: -1 }) - const latestTeachers = - await getUsers({ type: 'teacher', "entities.id": { $in: mapBy(allowedTeacherEntities, 'id') } }, 10, { registrationDate: -1 }) + const allowedStudentEntitiesIDS = mapBy(allowedStudentEntities, "id"); + const entitiesIDS = mapBy(entities, "id") || []; - const userCounts = await countAllowedUsers(user, entities) - const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } }); - const groupsCount = await countGroupsByEntities(mapBy(entities, "id")); - return { props: serialize({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, groupsCount }) }; + const students = await getUsers( + { type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } }, + 10, + { averageLevel: -1 }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); + const latestStudents = await getUsers( + { type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } }, + 10, + { registrationDate: -1 }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); + const latestTeachers = await getUsers( + { + type: "teacher", + "entities.id": { $in: mapBy(allowedTeacherEntities, "id") }, + }, + 10, + { registrationDate: -1 }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); + + const userCounts = await countAllowedUsers(user, entities); + + const assignmentsCount = await countEntitiesAssignments( + entitiesIDS, + { archived: { $ne: true } } + ); + + const groupsCount = await countGroupsByEntities(entitiesIDS); + + return { + props: serialize({ + user, + students, + latestStudents, + latestTeachers, + userCounts, + entities, + assignmentsCount, + groupsCount, + }), + }; }, sessionOptions); -export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) { - const totalCount = useMemo(() => - userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts]) - const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities]) - - const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics') - const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance') - - const router = useRouter(); - - return ( - <> - - EnCoach - - - - - - -
- {entities.length > 0 && ( -
- {mapBy(entities, "label")?.join(", ")} -
- )} -
- router.push("/users?type=student")} - Icon={BsPersonFill} - label="Students" - value={userCounts.student} - color="purple" - /> - router.push("/users?type=teacher")} - Icon={BsPencilSquare} - label="Teachers" - value={userCounts.teacher} - color="purple" - /> - router.push("/classrooms")} - Icon={BsPeople} - label="Classrooms" - value={groupsCount} - color="purple" - /> - router.push("/entities")} - label="Entities" - value={`${entities.length} - ${totalCount}/${totalLicenses}`} - color="purple" - /> - {allowedEntityStatistics.length > 0 && ( - router.push("/statistical")} - label="Entity Statistics" - value={allowedEntityStatistics.length} - color="purple" - /> - )} - {allowedStudentPerformance.length > 0 && ( - router.push("/users/performance")} - label="Student Performance" - value={userCounts.student} - color="purple" - /> - )} - - router.push("/assignments")} - label="Assignments" - value={assignmentsCount} - color="purple" - /> -
-
- -
- - - - - Object.keys(groupByExam(filterBy(stats, "user", b))).length - - Object.keys(groupByExam(filterBy(stats, "user", a))).length, - ) - } - title="Highest exam count students" - /> -
-
- +export default function Dashboard({ + user, + students, + latestStudents, + latestTeachers, + userCounts, + entities, + assignmentsCount, + stats = [], + groupsCount, +}: Props) { + const totalCount = useMemo( + () => + userCounts.corporate + + userCounts.mastercorporate + + userCounts.student + + userCounts.teacher, + [userCounts] ); + + const totalLicenses = useMemo( + () => + entities.reduce( + (acc, curr) => acc + parseInt(curr.licenses.toString()), + 0 + ), + [entities] + ); + + const allowedEntityStatistics = useAllowedEntities( + user, + entities, + "view_entity_statistics" + ); + const allowedStudentPerformance = useAllowedEntities( + user, + entities, + "view_student_performance" + ); + + const router = useRouter(); + + return ( + <> + + EnCoach + + + + + + <> +
+ {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} +
+ router.push("/users?type=student")} + Icon={BsPersonFill} + label="Students" + value={userCounts.student} + color="purple" + /> + router.push("/users?type=teacher")} + Icon={BsPencilSquare} + label="Teachers" + value={userCounts.teacher} + color="purple" + /> + router.push("/classrooms")} + Icon={BsPeople} + label="Classrooms" + value={groupsCount} + color="purple" + /> + router.push("/entities")} + label="Entities" + value={`${entities.length} - ${totalCount}/${totalLicenses}`} + color="purple" + /> + {allowedEntityStatistics.length > 0 && ( + router.push("/statistical")} + label="Entity Statistics" + value={allowedEntityStatistics.length} + color="purple" + /> + )} + {allowedStudentPerformance.length > 0 && ( + router.push("/users/performance")} + label="Student Performance" + value={userCounts.student} + color="purple" + /> + )} + + router.push("/assignments")} + label="Assignments" + value={assignmentsCount} + color="purple" + /> +
+
+ +
+ + + + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length + )} + title="Highest exam count students" + /> +
+ + + ); } diff --git a/src/pages/dashboard/developer.tsx b/src/pages/dashboard/developer.tsx index 1631bd61..03396bd2 100644 --- a/src/pages/dashboard/developer.tsx +++ b/src/pages/dashboard/developer.tsx @@ -1,189 +1,214 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; import { EntityWithRoles } from "@/interfaces/entity"; -import { Assignment } from "@/interfaces/results"; -import { Group, Stat, StudentUser, Type, User } from "@/interfaces/user"; +import { Stat, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; +import { filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { countEntitiesAssignments, getAssignments } from "@/utils/assignments.be"; -import { getEntitiesWithRoles } from "@/utils/entities.be"; -import { countGroups, getGroups } from "@/utils/groups.be"; +import { + countEntitiesAssignments, +} from "@/utils/assignments.be"; +import { getEntities } from "@/utils/entities.be"; +import { countGroups } from "@/utils/groups.be"; import { checkAccess } from "@/utils/permissions"; -import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; import { groupByExam } from "@/utils/stats"; -import { getStatsByUsers } from "@/utils/stats.be"; -import { countUsers, getUser, getUsers } from "@/utils/users.be"; +import { + countUsersByTypes, + getUsers, +} from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; -import { uniqBy } from "lodash"; import Head from "next/head"; import { useRouter } from "next/router"; -import { useMemo } from "react"; import { - BsBank, - BsClipboard2Data, - BsEnvelopePaper, - BsPencilSquare, - BsPeople, - BsPeopleFill, - BsPersonFill, - BsPersonFillGear, + BsBank, + BsEnvelopePaper, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; interface Props { - user: User; - students: User[]; - latestStudents: User[] - latestTeachers: User[] - entities: EntityWithRoles[]; - usersCount: { [key in Type]: number } - assignmentsCount: number; - stats: Stat[]; - groupsCount: number; + user: User; + students: User[]; + latestStudents: User[]; + latestTeachers: User[]; + entities: EntityWithRoles[]; + usersCount: { [key in Type]: number }; + assignmentsCount: number; + stats: Stat[]; + groupsCount: number; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user || !user.isVerified) return redirect("/login") + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); - if (!checkAccess(user, ["admin", "developer"])) return redirect("/") + if (!checkAccess(user, ["admin", "developer"])) return redirect("/"); - const students = await getUsers({ type: 'student' }, 10, { averageLevel: -1 }); - const usersCount = { - student: await countUsers({ type: "student" }), - teacher: await countUsers({ type: "teacher" }), - corporate: await countUsers({ type: "corporate" }), - mastercorporate: await countUsers({ type: "mastercorporate" }), - } + const students = await getUsers( + { type: "student" }, + 10, + { + averageLevel: -1, + }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); - const latestStudents = await getUsers({ type: 'student' }, 10, { registrationDate: -1 }) - const latestTeachers = await getUsers({ type: 'teacher' }, 10, { registrationDate: -1 }) + const usersCount = await countUsersByTypes([ + "student", + "teacher", + "corporate", + "mastercorporate", + ]); - const entities = await getEntitiesWithRoles(); - const assignmentsCount = await countEntitiesAssignments(mapBy(entities, 'id'), { archived: { $ne: true } }); - const groupsCount = await countGroups(); + const latestStudents = await getUsers( + { type: "student" }, + 10, + { + registrationDate: -1, + }, + {id:1, name: 1, email: 1, profilePicture: 1 } + ); + const latestTeachers = await getUsers( + { type: "teacher" }, + 10, + { + registrationDate: -1, + }, + { id:1,name: 1, email: 1, profilePicture: 1 } + ); - return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, groupsCount }) }; + const entities = await getEntities(undefined, { _id: 0, id: 1, label: 1 }); + const assignmentsCount = await countEntitiesAssignments( + mapBy(entities, "id"), + { archived: { $ne: true } } + ); + const groupsCount = await countGroups(); + + return { + props: serialize({ + user, + students, + latestStudents, + latestTeachers, + usersCount, + entities, + assignmentsCount, + groupsCount, + }), + }; }, sessionOptions); export default function Dashboard({ - user, - students = [], - latestStudents, - latestTeachers, - usersCount, - entities, - assignmentsCount, - stats = [], - groupsCount + user, + students = [], + latestStudents, + latestTeachers, + usersCount, + entities, + assignmentsCount, + stats = [], + groupsCount, }: Props) { - const router = useRouter(); + const router = useRouter(); - return ( - <> - - EnCoach - - - - - - -
- router.push("/users?type=student")} - Icon={BsPersonFill} - label="Students" - value={usersCount.student} - color="purple" - /> - router.push("/users?type=teacher")} - Icon={BsPencilSquare} - label="Teachers" - value={usersCount.teacher} - color="purple" - /> - router.push("/users?type=corporate")} - label="Corporates" - value={usersCount.corporate} - color="purple" - /> - router.push("/users?type=mastercorporate")} - label="Master Corporates" - value={usersCount.mastercorporate} - color="purple" - /> - router.push("/classrooms")} - label="Classrooms" - value={groupsCount} - color="purple" - /> - router.push("/entities")} - label="Entities" - value={entities.length} - color="purple" - /> - router.push("/statistical")} - label="Entity Statistics" - value={entities.length} - color="purple" - /> - router.push("/users/performance")} - label="Student Performance" - value={usersCount.student} - color="purple" - /> - router.push("/assignments")} - label="Assignments" - value={assignmentsCount} - color="purple" - /> -
+ return ( + <> + + EnCoach + + + + + + <> +
+ router.push("/users?type=student")} + Icon={BsPersonFill} + label="Students" + value={usersCount.student} + color="purple" + /> + router.push("/users?type=teacher")} + Icon={BsPencilSquare} + label="Teachers" + value={usersCount.teacher} + color="purple" + /> + router.push("/users?type=corporate")} + label="Corporates" + value={usersCount.corporate} + color="purple" + /> + router.push("/users?type=mastercorporate")} + label="Master Corporates" + value={usersCount.mastercorporate} + color="purple" + /> + router.push("/classrooms")} + label="Classrooms" + value={groupsCount} + color="purple" + /> + router.push("/entities")} + label="Entities" + value={entities.length} + color="purple" + /> + router.push("/statistical")} + label="Entity Statistics" + value={entities.length} + color="purple" + /> + router.push("/users/performance")} + label="Student Performance" + value={usersCount.student} + color="purple" + /> + router.push("/assignments")} + label="Assignments" + value={assignmentsCount} + color="purple" + /> +
-
- - - - - Object.keys(groupByExam(filterBy(stats, "user", b))).length - - Object.keys(groupByExam(filterBy(stats, "user", a))).length, - ) - } - title="Highest exam count students" - /> -
-
- - ); +
+ + + + + Object.keys(groupByExam(filterBy(stats, "user", b.id))).length - + Object.keys(groupByExam(filterBy(stats, "user", a.id))).length + )} + title="Highest exam count students" + /> +
+ + + ); } diff --git a/src/pages/dashboard/mastercorporate.tsx b/src/pages/dashboard/mastercorporate.tsx index 33044718..d8cc6432 100644 --- a/src/pages/dashboard/mastercorporate.tsx +++ b/src/pages/dashboard/mastercorporate.tsx @@ -1,203 +1,266 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; -import { Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; -import { Assignment } from "@/interfaces/results"; -import { Group, Stat, StudentUser, Type, User } from "@/interfaces/user"; +import { Stat, StudentUser, Type, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { dateSorter, filterBy, mapBy, redirect, serialize } from "@/utils"; +import { filterBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { countEntitiesAssignments, getEntitiesAssignments } from "@/utils/assignments.be"; +import { countEntitiesAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; -import { countGroupsByEntities, getGroupsByEntities } from "@/utils/groups.be"; -import { checkAccess, findAllowedEntities } from "@/utils/permissions"; -import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; +import { countGroupsByEntities } from "@/utils/groups.be"; +import { + checkAccess, + groupAllowedEntitiesByPermissions, +} from "@/utils/permissions"; import { groupByExam } from "@/utils/stats"; -import { getStatsByUsers } from "@/utils/stats.be"; -import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be"; -import { getEntitiesUsers } from "@/utils/users.be"; +import { countAllowedUsers, getUsers } from "@/utils/users.be"; import { clsx } from "clsx"; import { withIronSessionSsr } from "iron-session/next"; -import { uniqBy } from "lodash"; import moment from "moment"; import Head from "next/head"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useMemo } from "react"; import { - BsBank, - BsClipboard2Data, - BsClock, - BsEnvelopePaper, - BsPaperclip, - BsPencilSquare, - BsPeople, - BsPeopleFill, - BsPersonFill, - BsPersonFillGear, + BsBank, + BsClock, + BsEnvelopePaper, + BsPencilSquare, + BsPeople, + BsPeopleFill, + BsPersonFill, + BsPersonFillGear, } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; import { isAdmin } from "@/utils/users"; interface Props { - user: User; - students: StudentUser[] - latestStudents: User[] - latestTeachers: User[] - userCounts: { [key in Type]: number } - entities: EntityWithRoles[]; - assignmentsCount: number; - stats: Stat[]; - groupsCount: number; + user: User; + students: StudentUser[]; + latestStudents: User[]; + latestTeachers: User[]; + userCounts: { [key in Type]: number }; + entities: EntityWithRoles[]; + assignmentsCount: number; + stats: Stat[]; + groupsCount: number; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user || !user.isVerified) return redirect("/login") + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); - if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) return redirect("/") + if (!checkAccess(user, ["admin", "developer", "mastercorporate"])) + return redirect("/"); - const entityIDS = mapBy(user.entities, "id") || []; - const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); + const entityIDS = mapBy(user.entities, "id") || []; + const entities = await getEntitiesWithRoles( + isAdmin(user) ? undefined : entityIDS + ); + const { + ["view_students"]: allowedStudentEntities, + ["view_teachers"]: allowedTeacherEntities, + } = groupAllowedEntitiesByPermissions(user, entities, [ + "view_students", + "view_teachers", + ]); - const allowedStudentEntities = findAllowedEntities(user, entities, "view_students") - const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers") + const allowedStudentEntitiesIDS = mapBy(allowedStudentEntities, "id"); - const students = - await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 }); - const latestStudents = - await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { registrationDate: -1 }) - const latestTeachers = - await getUsers({ type: 'teacher', "entities.id": { $in: mapBy(allowedTeacherEntities, 'id') } }, 10, { registrationDate: -1 }) + const entitiesIDS = mapBy(entities, "id") || []; - const userCounts = await countAllowedUsers(user, entities) - const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } }); - const groupsCount = await countGroupsByEntities(mapBy(entities, "id")); + const students = await getUsers( + { type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } }, + 10, + { averageLevel: -1 }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); - return { props: serialize({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, groupsCount }) }; + const latestStudents = await getUsers( + { type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } }, + 10, + { registrationDate: -1 }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); + + const latestTeachers = await getUsers( + { + type: "teacher", + "entities.id": { $in: mapBy(allowedTeacherEntities, "id") }, + }, + 10, + { registrationDate: -1 }, + { id: 1, name: 1, email: 1, profilePicture: 1 } + ); + + const userCounts = await countAllowedUsers(user, entities); + + const assignmentsCount = await countEntitiesAssignments(entitiesIDS, { + archived: { $ne: true }, + }); + + const groupsCount = await countGroupsByEntities(entitiesIDS); + + return { + props: serialize({ + user, + students, + latestStudents, + latestTeachers, + userCounts, + entities, + assignmentsCount, + groupsCount, + }), + }; }, sessionOptions); -export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) { +export default function Dashboard({ + user, + students, + latestStudents, + latestTeachers, + userCounts, + entities, + assignmentsCount, + stats = [], + groupsCount, +}: Props) { + const totalCount = useMemo( + () => + userCounts.corporate + + userCounts.mastercorporate + + userCounts.student + + userCounts.teacher, + [userCounts] + ); - const totalCount = useMemo(() => - userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts]) + const totalLicenses = useMemo( + () => + entities.reduce( + (acc, curr) => acc + parseInt(curr.licenses.toString()), + 0 + ), + [entities] + ); - const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities]) + const router = useRouter(); - const router = useRouter(); + const allowedEntityStatistics = useAllowedEntities( + user, + entities, + "view_entity_statistics" + ); + const allowedStudentPerformance = useAllowedEntities( + user, + entities, + "view_student_performance" + ); - const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics') - const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance') + return ( + <> + + EnCoach + + + + + + <> +
+ router.push("/users?type=student")} + Icon={BsPersonFill} + label="Students" + value={userCounts.student} + color="purple" + /> + router.push("/users?type=teacher")} + Icon={BsPencilSquare} + label="Teachers" + value={userCounts.teacher} + color="purple" + /> + router.push("/users?type=corporate")} + Icon={BsBank} + label="Corporate Accounts" + value={userCounts.corporate} + color="purple" + /> + router.push("/classrooms")} + label="Classrooms" + value={groupsCount} + color="purple" + /> + router.push("/entities")} + label="Entities" + value={`${entities.length} - ${totalCount}/${totalLicenses}`} + color="purple" + /> + {allowedStudentPerformance.length > 0 && ( + router.push("/users/performance")} + label="Student Performance" + value={userCounts.student} + color="purple" + /> + )} + {allowedEntityStatistics.length > 0 && ( + router.push("/statistical")} + label="Entity Statistics" + value={allowedEntityStatistics.length} + color="purple" + /> + )} + router.push("/assignments")} + label="Assignments" + value={assignmentsCount} + className={clsx( + allowedEntityStatistics.length === 0 && "col-span-2" + )} + color="purple" + /> + +
- return ( - <> - - EnCoach - - - - - - -
- router.push("/users?type=student")} - Icon={BsPersonFill} - label="Students" - value={userCounts.student} - color="purple" - /> - router.push("/users?type=teacher")} - Icon={BsPencilSquare} - label="Teachers" - value={userCounts.teacher} - color="purple" - /> - router.push("/users?type=corporate")} - Icon={BsBank} - label="Corporate Accounts" - value={userCounts.corporate} - color="purple" - /> - router.push("/classrooms")} - label="Classrooms" - value={groupsCount} - color="purple" - /> - router.push("/entities")} - label="Entities" - value={`${entities.length} - ${totalCount}/${totalLicenses}`} - color="purple" - /> - {allowedStudentPerformance.length > 0 && ( - router.push("/users/performance")} - label="Student Performance" - value={userCounts.student} - color="purple" - /> - )} - {allowedEntityStatistics.length > 0 && ( - router.push("/statistical")} - label="Entity Statistics" - value={allowedEntityStatistics.length} - color="purple" - /> - )} - router.push("/assignments")} - label="Assignments" - value={assignmentsCount} - className={clsx(allowedEntityStatistics.length === 0 && "col-span-2")} - color="purple" - /> - -
- -
- - - - - Object.keys(groupByExam(filterBy(stats, "user", b))).length - - Object.keys(groupByExam(filterBy(stats, "user", a))).length, - ) - } - title="Highest exam count students" - /> -
-
- - ); +
+ + + + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length + )} + title="Highest exam count students" + /> +
+ + + ); } diff --git a/src/pages/dashboard/student.tsx b/src/pages/dashboard/student.tsx index 2a1f000c..90e4da8b 100644 --- a/src/pages/dashboard/student.tsx +++ b/src/pages/dashboard/student.tsx @@ -1,5 +1,4 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import Button from "@/components/Low/Button"; import ProgressBar from "@/components/Low/ProgressBar"; import InviteWithUserCard from "@/components/Medium/InviteWithUserCard"; @@ -10,267 +9,368 @@ import { Grading } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; import { Exam } from "@/interfaces/exam"; import { InviteWithEntity } from "@/interfaces/invite"; -import { Assignment } from "@/interfaces/results"; -import { Stat, User } from "@/interfaces/user"; +import { Assignment, AssignmentWithHasResults } from "@/interfaces/results"; +import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import useExamStore from "@/stores/exam"; import { findBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { activeAssignmentFilter } from "@/utils/assignments"; -import { getAssignmentsByAssignee } from "@/utils/assignments.be"; -import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be"; +import { getAssignmentsForStudent } from "@/utils/assignments.be"; +import { getEntities } from "@/utils/entities.be"; import { getExamsByIds } from "@/utils/exams.be"; import { getGradingSystemByEntity } from "@/utils/grading.be"; -import { convertInvitersToEntity, getInvitesByInvitee } from "@/utils/invites.be"; -import { countExamModules, countFullExams, MODULE_ARRAY, sortByModule, sortByModuleName } from "@/utils/moduleUtils"; +import { + convertInvitersToEntity, + getInvitesByInvitee, +} from "@/utils/invites.be"; +import { MODULE_ARRAY, sortByModule } from "@/utils/moduleUtils"; import { checkAccess } from "@/utils/permissions"; import { getGradingLabel } from "@/utils/score"; import { getSessionsByUser } from "@/utils/sessions.be"; -import { averageScore } from "@/utils/stats"; -import { getStatsByUser } from "@/utils/stats.be"; +import { getDetailedStatsByUser } from "@/utils/stats.be"; import clsx from "clsx"; import { withIronSessionSsr } from "iron-session/next"; import { capitalize, uniqBy } from "lodash"; import moment from "moment"; import Head from "next/head"; import { useRouter } from "next/router"; -import { useMemo } from "react"; -import { BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar } from "react-icons/bs"; +import { + BsBook, + BsClipboard, + BsFileEarmarkText, + BsHeadphones, + BsMegaphone, + BsPen, + BsPencil, + BsStar, +} from "react-icons/bs"; import { ToastContainer } from "react-toastify"; interface Props { - user: User; - entities: EntityWithRoles[]; - assignments: Assignment[]; - stats: Stat[]; - exams: Exam[]; - sessions: Session[]; - invites: InviteWithEntity[]; - grading: Grading; + user: User; + entities: EntityWithRoles[]; + assignments: AssignmentWithHasResults[]; + stats: { fullExams: number; uniqueModules: number; averageScore: number }; + exams: Exam[]; + sessions: Session[]; + invites: InviteWithEntity[]; + grading: Grading; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user || !user.isVerified) return redirect("/login") + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); - if (!checkAccess(user, ["admin", "developer", "student"])) - return redirect("/") + if (!checkAccess(user, ["admin", "developer", "student"])) + return redirect("/"); - const entityIDS = mapBy(user.entities, "id") || []; + const entityIDS = mapBy(user.entities, "id") || []; - const entities = await getEntitiesWithRoles(entityIDS); - const assignments = await getAssignmentsByAssignee(user.id, { archived: { $ne: true } }); - const stats = await getStatsByUser(user.id); - const sessions = await getSessionsByUser(user.id, 10); - const invites = await getInvitesByInvitee(user.id); - const grading = await getGradingSystemByEntity(entityIDS[0] || ""); + const entities = await getEntities(entityIDS, { _id: 0, label: 1 }); + const currentDate = moment().toISOString(); + const assignments = await getAssignmentsForStudent(user.id, currentDate); + const stats = await getDetailedStatsByUser(user.id, "stats"); - const formattedInvites = await Promise.all(invites.map(convertInvitersToEntity)); + const assignmentsIDs = mapBy(assignments, "id"); - const examIDs = uniqBy( - assignments.flatMap((a) => - a.exams.filter((e) => e.assignee === user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })), - ), - "key", - ); - const exams = await getExamsByIds(examIDs); + const sessions = await getSessionsByUser(user.id, 10, { + ["assignment.id"]: { $in: assignmentsIDs }, + }); + const invites = await getInvitesByInvitee(user.id); + const grading = await getGradingSystemByEntity(entityIDS[0] || "", { + _id: 0, + steps: 1, + }); - return { props: serialize({ user, entities, assignments, stats, exams, sessions, invites: formattedInvites, grading }) }; + const formattedInvites = await Promise.all( + invites.map(convertInvitersToEntity) + ); + const examIDs = uniqBy( + assignments.flatMap((a) => + a.exams.map((e: { module: string; id: string }) => ({ + module: e.module, + id: e.id, + key: `${e.module}_${e.id}`, + })) + ), + "key" + ); + const exams = examIDs.length > 0 ? await getExamsByIds(examIDs) : []; + + return { + props: serialize({ + user, + entities, + assignments, + stats, + exams, + sessions, + invites: formattedInvites, + grading, + }), + }; }, sessionOptions); -export default function Dashboard({ user, entities, assignments, stats, invites, grading, sessions, exams }: Props) { - const router = useRouter(); +export default function Dashboard({ + user, + entities, + assignments, + stats, + invites, + grading, + sessions, + exams, +}: Props) { + const router = useRouter(); - const dispatch = useExamStore((state) => state.dispatch); + const dispatch = useExamStore((state) => state.dispatch); - const startAssignment = (assignment: Assignment) => { - const assignmentExams = exams.filter(e => { - const exam = findBy(assignment.exams, 'id', e.id) - return !!exam && exam.module === e.module - }) + const startAssignment = (assignment: Assignment) => { + const assignmentExams = exams.filter((e) => { + const exam = findBy(assignment.exams, "id", e.id); + return !!exam && exam.module === e.module; + }); - if (assignmentExams.every((x) => !!x)) { - dispatch({ - type: "INIT_EXAM", payload: { - exams: assignmentExams.sort(sortByModule), - modules: mapBy(assignmentExams.sort(sortByModule), 'module'), - assignment - } - }) + if (assignmentExams.every((x) => !!x)) { + dispatch({ + type: "INIT_EXAM", + payload: { + exams: assignmentExams.sort(sortByModule), + modules: mapBy(assignmentExams.sort(sortByModule), "module"), + assignment, + }, + }); - router.push("/exam"); - } - }; + router.push("/exam"); + } + }; - const studentAssignments = useMemo(() => assignments.filter(activeAssignmentFilter), [assignments]); + return ( + <> + + EnCoach + + + + + + <> + {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} - return ( - <> - - EnCoach - - - - - - - {entities.length > 0 && ( -
- {mapBy(entities, "label")?.join(", ")} -
- )} + + ), + value: stats.fullExams, + label: "Exams", + tooltip: "Number of all conducted completed exams", + }, + { + icon: ( + + ), + value: stats.uniqueModules, + label: "Modules", + tooltip: + "Number of all exam modules performed including Level Test", + }, + { + icon: ( + + ), + value: `${stats?.averageScore.toFixed(2) || 0}%`, + label: "Average Score", + tooltip: "Average success rate for questions responded", + }, + ]} + /> - , - value: countFullExams(stats), - label: "Exams", - tooltip: "Number of all conducted completed exams", - }, - { - icon: , - value: countExamModules(stats), - label: "Modules", - tooltip: "Number of all exam modules performed including Level Test", - }, - { - icon: , - value: `${stats.length > 0 ? averageScore(stats) : 0}%`, - label: "Average Score", - tooltip: "Average success rate for questions responded", - }, - ]} - /> + {/* Assignments */} +
+ Assignments + + {assignments.length === 0 && + "Assignments will appear here. It seems that for now there are no assignments for you."} + {assignments.map((assignment) => ( +
+
+

+ {assignment.name} +

+ + + {moment(assignment.startDate).format("DD/MM/YY, HH:mm")} + + - + + {moment(assignment.endDate).format("DD/MM/YY, HH:mm")} + + +
+
+
+ {assignment.exams.map((e) => ( + + ))} +
+ {!assignment.hasResults && ( + <> +
+ +
+
x.assignment?.id === assignment.id + ).length > 0 && "tooltip" + )} + > + +
+ + )} + {assignment.hasResults && ( + + )} +
+
+ ))} +
+
- {/* Assignments */} -
- Assignments - - {studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."} - {studentAssignments - .sort((a, b) => moment(a.startDate).diff(b.startDate)) - .map((assignment) => ( -
r.user).includes(user.id) && "border-mti-green-light", - )} - key={assignment.id}> -
-

{assignment.name}

- - {moment(assignment.startDate).format("DD/MM/YY, HH:mm")} - - - {moment(assignment.endDate).format("DD/MM/YY, HH:mm")} - -
-
-
- {assignment.exams - .filter((e) => e.assignee === user.id) - .map((e) => e.module) - .sort(sortByModuleName) - .map((module) => ( - - ))} -
- {!assignment.results.map((r) => r.user).includes(user.id) && ( - <> -
- -
-
x.assignment?.id === assignment.id).length > 0 && "tooltip", - )}> - -
- - )} - {assignment.results.map((r) => r.user).includes(user.id) && ( - - )} -
-
- ))} -
-
+ {/* Invites */} + {invites.length > 0 && ( +
+ + {invites.map((invite) => ( + router.replace(router.asPath)} + /> + ))} + +
+ )} - {/* Invites */} - {invites.length > 0 && ( -
- - {invites.map((invite) => ( - router.replace(router.asPath)} /> - ))} - -
- )} - - {/* Score History */} -
- Score History -
- {MODULE_ARRAY.map((module) => { - const desiredLevel = user.desiredLevels[module] || 9; - const level = user.levels[module] || 0; - return ( -
-
-
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } -
-
- {capitalize(module)} - - {module === "level" && !!grading && `English Level: ${getGradingLabel(level, grading.steps)}`} - {module !== "level" && `Level ${level} / Level 9 (Desired Level: ${desiredLevel})`} - -
-
-
- -
-
- ); - })} -
-
-
- - ); + {/* Score History */} +
+ Score History +
+ {MODULE_ARRAY.map((module) => { + const desiredLevel = user.desiredLevels[module] || 9; + const level = user.levels[module] || 0; + return ( +
+
+
+ {module === "reading" && ( + + )} + {module === "listening" && ( + + )} + {module === "writing" && ( + + )} + {module === "speaking" && ( + + )} + {module === "level" && ( + + )} +
+
+ + {capitalize(module)} + + + {module === "level" && + !!grading && + `English Level: ${getGradingLabel( + level, + grading.steps + )}`} + {module !== "level" && + `Level ${level} / Level 9 (Desired Level: ${desiredLevel})`} + +
+
+
+ +
+
+ ); + })} +
+
+ + + ); } diff --git a/src/pages/dashboard/teacher.tsx b/src/pages/dashboard/teacher.tsx index 56fd56da..675805ec 100644 --- a/src/pages/dashboard/teacher.tsx +++ b/src/pages/dashboard/teacher.tsx @@ -1,8 +1,6 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import UserDisplayList from "@/components/UserDisplayList"; import IconCard from "@/components/IconCard"; -import { Module } from "@/interfaces"; import { EntityWithRoles } from "@/interfaces/entity"; import { Assignment } from "@/interfaces/results"; import { Group, Stat, User } from "@/interfaces/user"; @@ -12,138 +10,184 @@ import { getEntitiesAssignments } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; import { getGroupsByEntities } from "@/utils/groups.be"; import { checkAccess, findAllowedEntities } from "@/utils/permissions"; -import { calculateAverageLevel, calculateBandScore } from "@/utils/score"; +import { calculateAverageLevel } from "@/utils/score"; import { groupByExam } from "@/utils/stats"; import { getStatsByUsers } from "@/utils/stats.be"; -import { getEntitiesUsers } from "@/utils/users.be"; import { withIronSessionSsr } from "iron-session/next"; -import { uniqBy } from "lodash"; import Head from "next/head"; import { useRouter } from "next/router"; -import { useMemo } from "react"; -import { BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill, BsPersonFillGear } from "react-icons/bs"; +import { + BsEnvelopePaper, + BsPeople, + BsPersonFill, + BsPersonFillGear, +} from "react-icons/bs"; import { ToastContainer } from "react-toastify"; import { requestUser } from "@/utils/api"; import { useAllowedEntities } from "@/hooks/useEntityPermissions"; -import { filterAllowedUsers } from "@/utils/users.be"; +import { getEntitiesUsers } from "@/utils/users.be"; import { isAdmin } from "@/utils/users"; interface Props { - user: User; - users: User[]; - entities: EntityWithRoles[]; - assignments: Assignment[]; - stats: Stat[]; - groups: Group[]; + user: User; + students: User[]; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + groups: Group[]; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user || !user.isVerified) return redirect("/login") + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); - if (!checkAccess(user, ["admin", "developer", "teacher"])) - return redirect("/") + if (!checkAccess(user, ["admin", "developer", "teacher"])) + return redirect("/"); - const entityIDS = mapBy(user.entities, "id") || []; - const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); - const users = await filterAllowedUsers(user, entities) + const entityIDS = mapBy(user.entities, "id") || []; - const assignments = await getEntitiesAssignments(entityIDS); - const stats = await getStatsByUsers(users.map((u) => u.id)); - const groups = await getGroupsByEntities(entityIDS); + const entities = await getEntitiesWithRoles( + isAdmin(user) ? undefined : entityIDS + ); - return { props: serialize({ user, users, entities, assignments, stats, groups }) }; + const filteredEntities = findAllowedEntities(user, entities, "view_students"); + + const students = await getEntitiesUsers( + mapBy(filteredEntities, "id"), + { + type: "student", + }, + 0, + { + _id: 0, + id: 1, + name: 1, + email: 1, + profilePicture: 1, + levels: 1, + registrationDate: 1, + } + ); + + const assignments = await getEntitiesAssignments(entityIDS); + + const stats = await getStatsByUsers(students.map((u) => u.id)); + + const groups = await getGroupsByEntities(entityIDS); + + return { + props: serialize({ user, students, entities, assignments, stats, groups }), + }; }, sessionOptions); -export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) { - const students = useMemo(() => users.filter((u) => u.type === "student"), [users]); - const router = useRouter(); +export default function Dashboard({ + user, + students, + entities, + assignments, + stats, + groups, +}: Props) { + const router = useRouter(); - const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics') - const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance') + const allowedEntityStatistics = useAllowedEntities( + user, + entities, + "view_entity_statistics" + ); + const allowedStudentPerformance = useAllowedEntities( + user, + entities, + "view_student_performance" + ); - return ( - <> - - EnCoach - - - - - - -
- {entities.length > 0 && ( -
- {mapBy(entities, "label")?.join(", ")} -
- )} -
- router.push("/users?type=student")} - label="Students" - value={students.length} - color="purple" - /> - router.push("/classrooms")} - Icon={BsPeople} - label="Classrooms" - value={groups.length} - color="purple" - /> - {allowedStudentPerformance.length > 0 && ( - router.push("/users/performance")} - label="Student Performance" - value={students.length} - color="purple" - /> - )} - {allowedEntityStatistics.length > 0 && ( - router.push("/statistical")} - label="Entity Statistics" - value={allowedEntityStatistics.length} - color="purple" - /> - )} - router.push("/assignments")} - label="Assignments" - value={assignments.filter((a) => !a.archived).length} - color="purple" - /> -
-
+ return ( + <> + + EnCoach + + + + + + <> +
+ {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} +
+ router.push("/users?type=student")} + label="Students" + value={students.length} + color="purple" + /> + router.push("/classrooms")} + Icon={BsPeople} + label="Classrooms" + value={groups.length} + color="purple" + /> + {allowedStudentPerformance.length > 0 && ( + router.push("/users/performance")} + label="Student Performance" + value={students.length} + color="purple" + /> + )} + {allowedEntityStatistics.length > 0 && ( + router.push("/statistical")} + label="Entity Statistics" + value={allowedEntityStatistics.length} + color="purple" + /> + )} + router.push("/assignments")} + label="Assignments" + value={assignments.filter((a) => !a.archived).length} + color="purple" + /> +
+
-
- dateSorter(a, b, "desc", "registrationDate"))} - title="Latest Students" - /> - calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))} - title="Highest level students" - /> - - Object.keys(groupByExam(filterBy(stats, "user", b))).length - - Object.keys(groupByExam(filterBy(stats, "user", a))).length, - ) - } - title="Highest exam count students" - /> -
-
- - ); +
+ + dateSorter(a, b, "desc", "registrationDate") + )} + title="Latest Students" + /> + + calculateAverageLevel(b.levels) - + calculateAverageLevel(a.levels) + )} + title="Highest level students" + /> + + Object.keys(groupByExam(filterBy(stats, "user", b))).length - + Object.keys(groupByExam(filterBy(stats, "user", a))).length + )} + title="Highest exam count students" + /> +
+ + + ); } diff --git a/src/pages/entities/[id]/index.tsx b/src/pages/entities/[id]/index.tsx index 34b35978..473f7a0b 100644 --- a/src/pages/entities/[id]/index.tsx +++ b/src/pages/entities/[id]/index.tsx @@ -1,24 +1,20 @@ /* eslint-disable @next/next/no-img-element */ import CardList from "@/components/High/CardList"; -import Layout from "@/components/High/Layout"; import Select from "@/components/Low/Select"; import Input from "@/components/Low/Input"; import Checkbox from "@/components/Low/Checkbox"; import Tooltip from "@/components/Low/Tooltip"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; -import { useListSearch } from "@/hooks/useListSearch"; -import usePagination from "@/hooks/usePagination"; -import { Entity, EntityWithRoles, Role } from "@/interfaces/entity"; -import { GroupWithUsers, User } from "@/interfaces/user"; +import { EntityWithRoles, Role } from "@/interfaces/entity"; +import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { USER_TYPE_LABELS } from "@/resources/user"; import { findBy, mapBy, redirect, serialize } from "@/utils"; import { getEntityWithRoles } from "@/utils/entities.be"; -import { convertToUsers, getGroup } from "@/utils/groups.be"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { checkAccess, doesEntityAllow, getTypesOfUser } from "@/utils/permissions"; +import { doesEntityAllow } from "@/utils/permissions"; import { getUserName, isAdmin } from "@/utils/users"; -import { filterAllowedUsers, getEntitiesUsers, getEntityUsers, getLinkedUsers, getSpecificUsers, getUsers } from "@/utils/users.be"; +import { filterAllowedUsers, getEntitiesUsers, getEntityUsers, getUsers } from "@/utils/users.be"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; @@ -28,7 +24,7 @@ import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import { Divider } from "primereact/divider"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import ReactDatePicker from "react-datepicker"; import { CURRENCIES } from "@/resources/paypal"; @@ -37,11 +33,9 @@ import { BsChevronLeft, BsClockFill, BsEnvelopeFill, - BsFillPersonVcardFill, BsHash, BsPerson, BsPlus, - BsSquare, BsStopwatchFill, BsTag, BsTrash, @@ -344,7 +338,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) { - + <>
@@ -555,7 +549,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) { searchFields={[["name"], ["email"], ["corporateInformation", "companyInformation", "name"], ["role", "label"], ["type"]]} />
-
+ ); } diff --git a/src/pages/entities/[id]/roles/[role].tsx b/src/pages/entities/[id]/roles/[role].tsx index 5c41954f..907c157b 100644 --- a/src/pages/entities/[id]/roles/[role].tsx +++ b/src/pages/entities/[id]/roles/[role].tsx @@ -1,4 +1,3 @@ -import Layout from "@/components/High/Layout"; import Checkbox from "@/components/Low/Checkbox"; import Separator from "@/components/Low/Separator"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; @@ -152,7 +151,7 @@ interface Props { disableEdit?: boolean } -export default function Role({ user, entity, role, userCount, disableEdit }: Props) { +export default function EntityRole({ user, entity, role, userCount, disableEdit }: Props) { const [permissions, setPermissions] = useState(role.permissions) const [isLoading, setIsLoading] = useState(false); @@ -240,7 +239,7 @@ export default function Role({ user, entity, role, userCount, disableEdit }: Pro - + <>
@@ -388,7 +387,7 @@ export default function Role({ user, entity, role, userCount, disableEdit }: Pro
-
+ ); } diff --git a/src/pages/entities/[id]/roles/index.tsx b/src/pages/entities/[id]/roles/index.tsx index cb4a8639..bb9aab05 100644 --- a/src/pages/entities/[id]/roles/index.tsx +++ b/src/pages/entities/[id]/roles/index.tsx @@ -1,42 +1,25 @@ /* eslint-disable @next/next/no-img-element */ import CardList from "@/components/High/CardList"; -import Layout from "@/components/High/Layout"; -import Tooltip from "@/components/Low/Tooltip"; import { useEntityPermission } from "@/hooks/useEntityPermissions"; -import {useListSearch} from "@/hooks/useListSearch"; -import usePagination from "@/hooks/usePagination"; -import {Entity, EntityWithRoles, Role} from "@/interfaces/entity"; -import {GroupWithUsers, User} from "@/interfaces/user"; +import { EntityWithRoles, Role} from "@/interfaces/entity"; +import { User} from "@/interfaces/user"; import {sessionOptions} from "@/lib/session"; -import {USER_TYPE_LABELS} from "@/resources/user"; import { redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import {getEntityWithRoles} from "@/utils/entities.be"; -import {convertToUsers, getGroup} from "@/utils/groups.be"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import {checkAccess, doesEntityAllow, getTypesOfUser} from "@/utils/permissions"; -import {getUserName} from "@/utils/users"; -import {getEntityUsers, getLinkedUsers, getSpecificUsers} from "@/utils/users.be"; +import { doesEntityAllow} from "@/utils/permissions"; +import {getEntityUsers} from "@/utils/users.be"; import axios from "axios"; import clsx from "clsx"; import {withIronSessionSsr} from "iron-session/next"; -import moment from "moment"; import Head from "next/head"; import Link from "next/link"; import {useRouter} from "next/router"; import {Divider} from "primereact/divider"; -import {useEffect, useMemo, useState} from "react"; import { BsChevronLeft, - BsClockFill, - BsEnvelopeFill, - BsFillPersonVcardFill, BsPlus, - BsSquare, - BsStopwatchFill, - BsTag, - BsTrash, - BsX, } from "react-icons/bs"; import {toast, ToastContainer} from "react-toastify"; @@ -133,7 +116,7 @@ export default function Home({user, entity, roles, users}: Props) { - + <>
@@ -152,7 +135,7 @@ export default function Home({user, entity, roles, users}: Props) {
-
+ ); } diff --git a/src/pages/entities/create.tsx b/src/pages/entities/create.tsx index a3f92b54..0b974c8f 100644 --- a/src/pages/entities/create.tsx +++ b/src/pages/entities/create.tsx @@ -1,19 +1,16 @@ /* eslint-disable @next/next/no-img-element */ -import Layout from "@/components/High/Layout"; import Input from "@/components/Low/Input"; -import Select from "@/components/Low/Select"; import Tooltip from "@/components/Low/Tooltip"; import { useListSearch } from "@/hooks/useListSearch"; import usePagination from "@/hooks/usePagination"; -import { Entity, EntityWithRoles } from "@/interfaces/entity"; +import { Entity } from "@/interfaces/entity"; import { User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; import { USER_TYPE_LABELS } from "@/resources/user"; -import { mapBy, redirect, serialize } from "@/utils"; -import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be"; +import { redirect, serialize } from "@/utils"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { getUserName } from "@/utils/users"; -import { getLinkedUsers, getUsers } from "@/utils/users.be"; +import { getUsers } from "@/utils/users.be"; import axios from "axios"; import clsx from "clsx"; import { withIronSessionSsr } from "iron-session/next"; @@ -26,7 +23,6 @@ import { useState } from "react"; import { BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill } from "react-icons/bs"; import { toast, ToastContainer } from "react-toastify"; import { requestUser } from "@/utils/api"; -import { findAllowedEntities } from "@/utils/permissions"; export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const user = await requestUser(req, res) @@ -91,7 +87,7 @@ export default function Home({ user, users }: Props) { - + <>
@@ -178,7 +174,7 @@ export default function Home({ user, users }: Props) { ))}
-
+ ); } diff --git a/src/pages/entities/index.tsx b/src/pages/entities/index.tsx index 8f588ced..d9ef0924 100644 --- a/src/pages/entities/index.tsx +++ b/src/pages/entities/index.tsx @@ -3,15 +3,12 @@ import Head from "next/head"; import { withIronSessionSsr } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; import { ToastContainer } from "react-toastify"; -import Layout from "@/components/High/Layout"; -import { GroupWithUsers, User } from "@/interfaces/user"; +import { User } from "@/interfaces/user"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { getUserName, isAdmin } from "@/utils/users"; -import { convertToUsers, getGroupsForUser } from "@/utils/groups.be"; -import { countEntityUsers, getEntityUsers, getSpecificUsers, getUsers } from "@/utils/users.be"; -import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions"; +import { countEntityUsers, getEntityUsers } from "@/utils/users.be"; +import { findAllowedEntities } from "@/utils/permissions"; import Link from "next/link"; -import { uniq } from "lodash"; import { BsBank, BsPlus } from "react-icons/bs"; import CardList from "@/components/High/CardList"; import { getEntitiesWithRoles } from "@/utils/entities.be"; @@ -20,99 +17,126 @@ import Separator from "@/components/Low/Separator"; import { requestUser } from "@/utils/api"; import { mapBy, redirect, serialize } from "@/utils"; -type EntitiesWithCount = { entity: EntityWithRoles; users: User[]; count: number }; +type EntitiesWithCount = { + entity: EntityWithRoles; + users: User[]; + count: number; +}; export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") + const user = await requestUser(req, res); + if (!user) return redirect("/login"); - if (shouldRedirectHome(user)) return redirect("/") + if (shouldRedirectHome(user)) return redirect("/"); - const entityIDs = mapBy(user.entities, 'id') - const entities = await getEntitiesWithRoles(["admin", "developer"].includes(user.type) ? undefined : entityIDs); - const allowedEntities = findAllowedEntities(user, entities, 'view_entities') + const entityIDs = mapBy(user.entities, "id"); + const entities = await getEntitiesWithRoles( + ["admin", "developer"].includes(user.type) ? undefined : entityIDs + ); + const allowedEntities = findAllowedEntities(user, entities, "view_entities"); - const entitiesWithCount = await Promise.all( - allowedEntities.map(async (e) => ({ - entity: e, - count: await countEntityUsers(e.id, { type: { $in: ["student", "teacher", "corporate", "mastercorporate"] } }), - users: await getEntityUsers(e.id, 5, { type: { $in: ["student", "teacher", "corporate", "mastercorporate"] } }) - })), - ); + const entitiesWithCount = await Promise.all( + allowedEntities.map(async (e) => ({ + entity: e, + count: await countEntityUsers(e.id, { + type: { $in: ["student", "teacher", "corporate", "mastercorporate"] }, + }), + users: await getEntityUsers(e.id, 5, { + type: { $in: ["student", "teacher", "corporate", "mastercorporate"] }, + }), + })) + ); - return { - props: serialize({ user, entities: entitiesWithCount }), - }; + return { + props: serialize({ user, entities: entitiesWithCount }), + }; }, sessionOptions); const SEARCH_FIELDS: string[][] = [["entity", "label"]]; interface Props { - user: User; - entities: EntitiesWithCount[]; + user: User; + entities: EntitiesWithCount[]; } export default function Home({ user, entities }: Props) { - const renderCard = ({ entity, users, count }: EntitiesWithCount) => ( - -
- - Entity - {entity.label} - - - Members - {count}{isAdmin(user) && ` / ${entity.licenses || 0}`} - - - {users.map(getUserName).join(", ")}{' '} - {count > 5 ? and {count - 5} more : ""} - -
-
- -
- - ); + const renderCard = ({ entity, users, count }: EntitiesWithCount) => ( + +
+ + + Entity + + {entity.label} + + + + Members + + + {count} + {isAdmin(user) && ` / ${entity.licenses || 0}`} + + + + {users.map(getUserName).join(", ")}{" "} + {count > 5 ? ( + + and {count - 5} more + + ) : ( + "" + )} + +
+
+ +
+ + ); - const firstCard = () => ( - - - Create Entity - - ); + const firstCard = () => ( + + + Create Entity + + ); - return ( - <> - - Entities | EnCoach - - - - - - -
-
-

Entities

- -
+ return ( + <> + + Entities | EnCoach + + + + + + <> +
+
+

Entities

+ +
- - list={entities} - searchFields={SEARCH_FIELDS} - renderCard={renderCard} - firstCard={["admin", "developer"].includes(user.type) ? firstCard : undefined} - /> -
- - - ); + + list={entities} + searchFields={SEARCH_FIELDS} + renderCard={renderCard} + firstCard={ + ["admin", "developer"].includes(user.type) ? firstCard : undefined + } + /> +
+ + + ); } diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index 301506f9..99f1e63c 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -20,7 +20,6 @@ import { useRouter } from "next/router"; import { getSessionByAssignment } from "@/utils/sessions.be"; import { Session } from "@/hooks/useSessions"; import { activeAssignmentFilter } from "@/utils/assignments"; -import { checkAccess } from "@/utils/permissions"; export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => { const user = await requestUser(req, res) diff --git a/src/pages/generation.tsx b/src/pages/generation.tsx index 595a6c39..94f9de64 100644 --- a/src/pages/generation.tsx +++ b/src/pages/generation.tsx @@ -2,24 +2,22 @@ import Head from "next/head"; import { withIronSessionSsr } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; -import { toast, ToastContainer } from "react-toastify"; -import Layout from "@/components/High/Layout"; +import { ToastContainer } from "react-toastify"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import { Radio, RadioGroup } from "@headlessui/react"; import clsx from "clsx"; import { MODULE_ARRAY } from "@/utils/moduleUtils"; import { capitalize } from "lodash"; import Input from "@/components/Low/Input"; -import { checkAccess, findAllowedEntities } from "@/utils/permissions"; +import { findAllowedEntities } from "@/utils/permissions"; import { User } from "@/interfaces/user"; import useExamEditorStore from "@/stores/examEditor"; import ExamEditorStore from "@/stores/examEditor/types"; import ExamEditor from "@/components/ExamEditor"; -import MultipleAudioUploader from "@/components/ExamEditor/Shared/AudioEdit"; import { mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { Module } from "@/interfaces"; -import { getExam, getExams } from "@/utils/exams.be"; +import { getExam, } from "@/utils/exams.be"; import { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam"; import { useEffect, useState } from "react"; import { getEntitiesWithRoles } from "@/utils/entities.be"; @@ -157,7 +155,7 @@ export default function Generation({ id, user, exam, examModule, permissions }: {user && ( - + <>

Exam Editor

-
+ )} ); diff --git a/src/pages/official-exam.tsx b/src/pages/official-exam.tsx index b1695263..d472f9d9 100644 --- a/src/pages/official-exam.tsx +++ b/src/pages/official-exam.tsx @@ -1,6 +1,5 @@ /* eslint-disable @next/next/no-img-element */ import AssignmentCard from "@/components/High/AssignmentCard"; -import Layout from "@/components/High/Layout"; import Button from "@/components/Low/Button"; import Separator from "@/components/Low/Separator"; import ProfileSummary from "@/components/ProfileSummary"; @@ -15,7 +14,10 @@ import { sessionOptions } from "@/lib/session"; import useExamStore from "@/stores/exam"; import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments"; +import { + activeAssignmentFilter, + futureAssignmentFilter, +} from "@/utils/assignments"; import { getAssignmentsByAssignee } from "@/utils/assignments.be"; import { getEntitiesWithRoles } from "@/utils/entities.be"; import { getExamsByIds } from "@/utils/exams.be"; @@ -34,142 +36,187 @@ import { BsArrowRepeat } from "react-icons/bs"; import { ToastContainer } from "react-toastify"; interface Props { - user: User; - entities: EntityWithRoles[]; - assignments: Assignment[]; - stats: Stat[]; - exams: Exam[]; - sessions: Session[]; - invites: InviteWithEntity[]; - grading: Grading; + user: User; + entities: EntityWithRoles[]; + assignments: Assignment[]; + stats: Stat[]; + exams: Exam[]; + sessions: Session[]; + invites: InviteWithEntity[]; + grading: Grading; } export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - const destination = Buffer.from(req.url || "/").toString("base64") - if (!user) return redirect(`/login?destination=${destination}`) + const user = await requestUser(req, res); + const destination = Buffer.from(req.url || "/").toString("base64"); + if (!user) return redirect(`/login?destination=${destination}`); - if (!checkAccess(user, ["admin", "developer", "student"])) - return redirect("/") + if (!checkAccess(user, ["admin", "developer", "student"])) + return redirect("/"); - const entityIDS = mapBy(user.entities, "id") || []; + const entityIDS = mapBy(user.entities, "id") || []; - const entities = await getEntitiesWithRoles(entityIDS); - const assignments = await getAssignmentsByAssignee(user.id, { archived: { $ne: true } }); - const sessions = await getSessionsByUser(user.id, 0, { "assignment.id": { $in: mapBy(assignments, 'id') } }); + const entities = await getEntitiesWithRoles(entityIDS); + const assignments = await getAssignmentsByAssignee(user.id, { + archived: { $ne: true }, + }); + const sessions = await getSessionsByUser(user.id, 0, { + "assignment.id": { $in: mapBy(assignments, "id") }, + }); - const examIDs = uniqBy( - assignments.flatMap((a) => - filterBy(a.exams, 'assignee', user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })), - ), - "key", - ); - const exams = await getExamsByIds(examIDs); + const examIDs = uniqBy( + assignments.flatMap((a) => + filterBy(a.exams, "assignee", user.id).map( + (e: any) => ({ + module: e.module, + id: e.id, + key: `${e.module}_${e.id}`, + }) + ) + ), + "key" + ); + const exams = await getExamsByIds(examIDs); - return { props: serialize({ user, entities, assignments, exams, sessions }) }; + return { props: serialize({ user, entities, assignments, exams, sessions }) }; }, sessionOptions); -const destination = Buffer.from("/official-exam").toString("base64") +const destination = Buffer.from("/official-exam").toString("base64"); -export default function OfficialExam({ user, entities, assignments, sessions, exams }: Props) { - const [isLoading, setIsLoading] = useState(false) +export default function OfficialExam({ + user, + entities, + assignments, + sessions, + exams, +}: Props) { + const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); + const router = useRouter(); - const dispatch = useExamStore((state) => state.dispatch); + const dispatch = useExamStore((state) => state.dispatch); - const reload = () => { - setIsLoading(true) - router.replace(router.asPath) - setTimeout(() => setIsLoading(false), 500) - } + const reload = () => { + setIsLoading(true); + router.replace(router.asPath); + setTimeout(() => setIsLoading(false), 500); + }; - const startAssignment = (assignment: Assignment) => { - const assignmentExams = exams.filter(e => { - const exam = findBy(assignment.exams, 'id', e.id) - return !!exam && exam.module === e.module - }) + const startAssignment = (assignment: Assignment) => { + const assignmentExams = exams.filter((e) => { + const exam = findBy(assignment.exams, "id", e.id); + return !!exam && exam.module === e.module; + }); - if (assignmentExams.every((x) => !!x)) { - dispatch({ - type: "INIT_EXAM", payload: { - exams: assignmentExams.sort(sortByModule), - modules: mapBy(assignmentExams.sort(sortByModule), 'module'), - assignment - } - }) - router.push(`/exam?assignment=${assignment.id}&destination=${destination}`); - } - }; + if (assignmentExams.every((x) => !!x)) { + dispatch({ + type: "INIT_EXAM", + payload: { + exams: assignmentExams.sort(sortByModule), + modules: mapBy(assignmentExams.sort(sortByModule), "module"), + assignment, + }, + }); + router.push( + `/exam?assignment=${assignment.id}&destination=${destination}` + ); + } + }; - const loadSession = async (session: Session) => { - dispatch({type: "SET_SESSION", payload: {session}}); - router.push(`/exam?assignment=${session.assignment?.id}&destination=${destination}`); - }; + const loadSession = async (session: Session) => { + dispatch({ type: "SET_SESSION", payload: { session } }); + router.push( + `/exam?assignment=${session.assignment?.id}&destination=${destination}` + ); + }; - const logout = async () => { - axios.post("/api/logout").finally(() => { - setTimeout(() => router.reload(), 500); - }); - }; + const logout = async () => { + axios.post("/api/logout").finally(() => { + setTimeout(() => router.reload(), 500); + }); + }; - const studentAssignments = useMemo(() => [ - ...assignments.filter(activeAssignmentFilter), ...assignments.filter(futureAssignmentFilter)], - [assignments] - ); + const studentAssignments = useMemo( + () => [ + ...assignments.filter(activeAssignmentFilter), + ...assignments.filter(futureAssignmentFilter), + ], + [assignments] + ); - const assignmentSessions = useMemo(() => sessions.filter(s => mapBy(studentAssignments, 'id').includes(s.assignment?.id || "")), [sessions, studentAssignments]) + const assignmentSessions = useMemo( + () => + sessions.filter((s) => + mapBy(studentAssignments, "id").includes(s.assignment?.id || "") + ), + [sessions, studentAssignments] + ); - return ( - <> - - EnCoach - - - - - - - {entities.length > 0 && ( -
- {mapBy(entities, "label")?.join(", ")} -
- )} + return ( + <> + + EnCoach + + + + + + <> + {entities.length > 0 && ( +
+ {mapBy(entities, "label")?.join(", ")} +
+ )} - + - + - {/* Assignments */} -
-
- Assignments - -
- - {studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."} - {studentAssignments - .sort((a, b) => moment(a.startDate).diff(b.startDate)) - .map((a) => - s.assignment?.id === a.id)} - startAssignment={startAssignment} - resumeAssignment={loadSession} - /> - )} - -
+ {/* Assignments */} +
+
+ + Assignments + + +
+ + {studentAssignments.length === 0 && + "Assignments will appear here. It seems that for now there are no assignments for you."} + {studentAssignments + .sort((a, b) => moment(a.startDate).diff(b.startDate)) + .map((a) => ( + s.assignment?.id === a.id + )} + startAssignment={startAssignment} + resumeAssignment={loadSession} + /> + ))} + +
- -
- - ); + + + + ); } diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index a1594bc2..618d9621 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -2,14 +2,12 @@ import Head from "next/head"; import { withIronSessionSsr } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; -import useUser from "@/hooks/useUser"; import { toast, ToastContainer } from "react-toastify"; -import Layout from "@/components/High/Layout"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import usePayments from "@/hooks/usePayments"; import usePaypalPayments from "@/hooks/usePaypalPayments"; import { Payment, PaypalPayment } from "@/interfaces/paypal"; -import { CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable } from "@tanstack/react-table"; +import { createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable } from "@tanstack/react-table"; import { CURRENCIES } from "@/resources/paypal"; import { BsTrash } from "react-icons/bs"; import axios from "axios"; @@ -943,7 +941,7 @@ export default function PaymentRecord({ user, entities }: Props) { {user && ( - + <> {getUserModal()} setIsCreatingPayment(false)}> - + )} ); diff --git a/src/pages/permissions/[id].tsx b/src/pages/permissions/[id].tsx index e7ac515c..6e79b2d3 100644 --- a/src/pages/permissions/[id].tsx +++ b/src/pages/permissions/[id].tsx @@ -1,188 +1,219 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import {useEffect, useState} from "react"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import {Permission, PermissionType} from "@/interfaces/permissions"; -import {getPermissionDoc} from "@/utils/permissions.be"; -import {User} from "@/interfaces/user"; -import Layout from "@/components/High/Layout"; -import {getUsers} from "@/utils/users.be"; -import {BsTrash} from "react-icons/bs"; +import React, { useEffect, useState } from "react"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { Permission, PermissionType } from "@/interfaces/permissions"; +import { getPermissionDoc } from "@/utils/permissions.be"; +import { User } from "@/interfaces/user"; +import { LayoutContext } from "@/components/High/Layout"; +import { getUsers } from "@/utils/users.be"; +import { BsTrash } from "react-icons/bs"; import Select from "@/components/Low/Select"; import Button from "@/components/Low/Button"; import axios from "axios"; -import {toast, ToastContainer} from "react-toastify"; -import {Type as UserType} from "@/interfaces/user"; -import {getGroups} from "@/utils/groups.be"; +import { toast, ToastContainer } from "react-toastify"; +import { Type as UserType } from "@/interfaces/user"; +import { getGroups } from "@/utils/groups.be"; import { requestUser } from "@/utils/api"; import { redirect } from "@/utils"; interface BasicUser { - id: string; - name: string; - type: UserType; + id: string; + name: string; + type: UserType; } interface PermissionWithBasicUsers { - id: string; - type: PermissionType; - users: BasicUser[]; + id: string; + type: PermissionType; + users: BasicUser[]; } -export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") +export const getServerSideProps = withIronSessionSsr( + async ({ req, res, params }) => { + const user = await requestUser(req, res); + if (!user) return redirect("/login"); - if (shouldRedirectHome(user)) return redirect("/") + if (shouldRedirectHome(user)) return redirect("/"); - if (!params?.id) return redirect("/permissions") + if (!params?.id) return redirect("/permissions"); - // Fetch data from external API - const permission: Permission = await getPermissionDoc(params.id as string); + // Fetch data from external API + const permission: Permission = await getPermissionDoc(params.id as string); - const allUserData: User[] = await getUsers(); - const groups = await getGroups(); + const allUserData: User[] = await getUsers(); + const groups = await getGroups(); - const userGroups = groups.filter((x) => x.admin === user.id); - const filteredGroups = - user.type === "corporate" - ? userGroups - : user.type === "mastercorporate" - ? groups.filter((x) => userGroups.flatMap((y) => y.participants).includes(x.admin)) - : groups; + const userGroups = groups.filter((x) => x.admin === user.id); + const filteredGroups = + user.type === "corporate" + ? userGroups + : user.type === "mastercorporate" + ? groups.filter((x) => + userGroups.flatMap((y) => y.participants).includes(x.admin) + ) + : groups; - const users = allUserData.map((u) => ({ - id: u.id, - name: u.name, - type: u.type, - })) as BasicUser[]; + const users = allUserData.map((u) => ({ + id: u.id, + name: u.name, + type: u.type, + })) as BasicUser[]; - const filteredUsers = ["mastercorporate", "corporate"].includes(user.type) - ? users.filter((u) => filteredGroups.flatMap((g) => g.participants).includes(u.id)) - : users; + const filteredUsers = ["mastercorporate", "corporate"].includes(user.type) + ? users.filter((u) => + filteredGroups.flatMap((g) => g.participants).includes(u.id) + ) + : users; - // const res = await fetch("api/permissions"); - // const permissions: Permission[] = await res.json(); - // Pass data to the page via props - const usersData: BasicUser[] = permission.users.reduce((acc: BasicUser[], userId) => { - const user = filteredUsers.find((u) => u.id === userId) as BasicUser; - if (!!user) acc.push(user); - return acc; - }, []); + // const res = await fetch("api/permissions"); + // const permissions: Permission[] = await res.json(); + // Pass data to the page via props + const usersData: BasicUser[] = permission.users.reduce( + (acc: BasicUser[], userId) => { + const user = filteredUsers.find((u) => u.id === userId) as BasicUser; + if (!!user) acc.push(user); + return acc; + }, + [] + ); - return { - props: { - // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), - permission: { - ...permission, - id: params.id, - users: usersData, - }, - user, - users: filteredUsers, - }, - }; -}, sessionOptions); + return { + props: { + // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), + permission: { + ...permission, + id: params.id, + users: usersData, + }, + user, + users: filteredUsers, + }, + }; + }, + sessionOptions +); interface Props { - permission: PermissionWithBasicUsers; - user: User; - users: BasicUser[]; + permission: PermissionWithBasicUsers; + user: User; + users: BasicUser[]; } export default function Page(props: Props) { - const {permission, user, users} = props; + const { permission, user, users } = props; - const [selectedUsers, setSelectedUsers] = useState(() => permission.users.map((u) => u.id)); + const [selectedUsers, setSelectedUsers] = useState(() => + permission.users.map((u) => u.id) + ); - const onChange = (value: any) => { - setSelectedUsers((prev) => { - if (value?.value) { - return [...prev, value?.value]; - } - return prev; - }); - }; - const removeUser = (id: string) => { - setSelectedUsers((prev) => prev.filter((u) => u !== id)); - }; + const onChange = (value: any) => { + setSelectedUsers((prev) => { + if (value?.value) { + return [...prev, value?.value]; + } + return prev; + }); + }; + const removeUser = (id: string) => { + setSelectedUsers((prev) => prev.filter((u) => u !== id)); + }; - const update = async () => { - try { - await axios.patch(`/api/permissions/${permission.id}`, { - users: selectedUsers, - }); - toast.success("Permission updated"); - } catch (err) { - toast.error("Failed to update permission"); - } - }; + const update = async () => { + try { + await axios.patch(`/api/permissions/${permission.id}`, { + users: selectedUsers, + }); + toast.success("Permission updated"); + } catch (err) { + toast.error("Failed to update permission"); + } + }; - return ( - <> - - EnCoach - - - - - - -
-

Permission: {permission.type as string}

-
- !selectedUsers.includes(u.id)) + .map((u) => ({ + label: `${u?.type}-${u?.name}`, + value: u.id, + }))} + onChange={onChange} + /> + +
+
+
+

Blacklisted Users

+
+ {selectedUsers.map((userId) => { + const user = users.find((u) => u.id === userId); + return ( +
+ + {user?.type}-{user?.name} + + removeUser(userId)} + size={20} + /> +
+ ); + })} +
+
+
+

Whitelisted Users

+
+ {users + .filter((user) => !selectedUsers.includes(user.id)) + .map((user) => { + return ( +
+ + {user?.type}-{user?.name} + +
+ ); + })} +
+
+
+
+ + + ); } diff --git a/src/pages/permissions/index.tsx b/src/pages/permissions/index.tsx index 4ca504b8..50e71247 100644 --- a/src/pages/permissions/index.tsx +++ b/src/pages/permissions/index.tsx @@ -1,73 +1,85 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import {withIronSessionSsr} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import {Permission} from "@/interfaces/permissions"; -import {getPermissionDocs} from "@/utils/permissions.be"; -import {User} from "@/interfaces/user"; -import Layout from "@/components/High/Layout"; +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { Permission } from "@/interfaces/permissions"; +import { getPermissionDocs } from "@/utils/permissions.be"; +import { User } from "@/interfaces/user"; +import { LayoutContext } from "@/components/High/Layout"; import PermissionList from "@/components/PermissionList"; import { requestUser } from "@/utils/api"; import { redirect } from "@/utils"; +import React from "react"; -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") +export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { + const user = await requestUser(req, res); + if (!user) return redirect("/login"); - if (shouldRedirectHome(user)) return redirect("/") + if (shouldRedirectHome(user)) return redirect("/"); - // Fetch data from external API - const permissions: Permission[] = await getPermissionDocs(); - const filteredPermissions = permissions.filter((p) => { - const permissionType = p.type.toString().toLowerCase(); + // Fetch data from external API + const permissions: Permission[] = await getPermissionDocs(); + const filteredPermissions = permissions.filter((p) => { + const permissionType = p.type.toString().toLowerCase(); - if (user.type === "corporate") return !permissionType.includes("corporate") && !permissionType.includes("admin"); - if (user.type === "mastercorporate") return !permissionType.includes("mastercorporate") && !permissionType.includes("admin"); + if (user.type === "corporate") + return ( + !permissionType.includes("corporate") && + !permissionType.includes("admin") + ); + if (user.type === "mastercorporate") + return ( + !permissionType.includes("mastercorporate") && + !permissionType.includes("admin") + ); - return true; - }); + return true; + }); - // const res = await fetch("api/permissions"); - // const permissions: Permission[] = await res.json(); - // Pass data to the page via props - return { - props: { - // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), - permissions: filteredPermissions.map((p) => { - const {users, ...rest} = p; - return rest; - }), - user, - }, - }; + // const res = await fetch("api/permissions"); + // const permissions: Permission[] = await res.json(); + // Pass data to the page via props + return { + props: { + // permissions: permissions.map((p) => ({ id: p.id, type: p.type })), + permissions: filteredPermissions.map((p) => { + const { users, ...rest } = p; + return rest; + }), + user, + }, + }; }, sessionOptions); interface Props { - permissions: Permission[]; - user: User; + permissions: Permission[]; + user: User; } export default function Page(props: Props) { - const {permissions, user} = props; + const { permissions, user } = props; - return ( - <> - - EnCoach - - - - - -

Permissions

-
- -
-
- - ); + const { setClassName } = React.useContext(LayoutContext); + React.useEffect(() => setClassName("gap-6"), [setClassName]); + + return ( + <> + + EnCoach + + + + + <> +

Permissions

+
+ +
+ + + ); } diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 9bb4c9f9..9c1ec17a 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -2,10 +2,16 @@ import Head from "next/head"; import { withIronSessionSsr } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; -import { ChangeEvent, Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from "react"; +import { + ChangeEvent, + Dispatch, + ReactNode, + SetStateAction, + useRef, + useState, +} from "react"; import useUser from "@/hooks/useUser"; import { toast, ToastContainer } from "react-toastify"; -import Layout from "@/components/High/Layout"; import Input from "@/components/Low/Input"; import Button from "@/components/Low/Button"; import Link from "next/link"; @@ -13,28 +19,23 @@ import axios from "axios"; import { ErrorMessage } from "@/constants/errors"; import clsx from "clsx"; import { - CorporateUser, - EmploymentStatus, - EMPLOYMENT_STATUS, - Gender, - User, - DemographicInformation, - MasterCorporateUser, - Group, + CorporateUser, + EmploymentStatus, + Gender, + User, + DemographicInformation, + MasterCorporateUser, } from "@/interfaces/user"; import CountrySelect from "@/components/Low/CountrySelect"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import moment from "moment"; import { BsCamera, BsQuestionCircleFill } from "react-icons/bs"; import { USER_TYPE_LABELS } from "@/resources/user"; -import useGroups from "@/hooks/useGroups"; -import useUsers from "@/hooks/useUsers"; import { convertBase64, redirect } from "@/utils"; import { Divider } from "primereact/divider"; import GenderInput from "@/components/High/GenderInput"; import EmploymentStatusInput from "@/components/High/EmploymentStatusInput"; import TimezoneSelect from "@/components/Low/TImezoneSelect"; -import Modal from "@/components/Modal"; import { Module } from "@/interfaces"; import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector"; import Select from "@/components/Low/Select"; @@ -42,594 +43,741 @@ import { InstructorGender } from "@/interfaces/exam"; import { capitalize } from "lodash"; import TopicModal from "@/components/Medium/TopicModal"; import { v4 } from "uuid"; -import { checkAccess, getTypesOfUser } from "@/utils/permissions"; +import { checkAccess } from "@/utils/permissions"; import { getParticipantGroups, getUserCorporate } from "@/utils/groups.be"; -import { InferGetServerSidePropsType } from "next"; -import { getUsers } from "@/utils/users.be"; +import { countUsers, getUser } from "@/utils/users.be"; import { requestUser } from "@/utils/api"; export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") + const user = await requestUser(req, res); + if (!user) return redirect("/login"); - if (shouldRedirectHome(user)) return redirect("/") + if (shouldRedirectHome(user)) return redirect("/"); + const linkedCorporate = (await getUserCorporate(user.id)) || null; + const groups = ( + await getParticipantGroups(user.id, { _id: 0, admin: 1 }) + ).map((group) => group.admin); + const referralAgent = + user.type === "corporate" && user.corporateInformation.referralAgent + ? await getUser(user.corporateInformation.referralAgent, { + _id: 0, + name: 1, + email: 1, + demographicInformation: 1, + }) + : null; - return { - props: { - user, - linkedCorporate: (await getUserCorporate(user.id)) || null, - groups: await getParticipantGroups(user.id), - users: await getUsers(), - }, - }; + const hasBenefitsFromUniversity = + (await countUsers({ + id: { $in: groups }, + type: "corporate", + })) > 0; + + return { + props: { + user, + linkedCorporate, + hasBenefitsFromUniversity, + referralAgent, + }, + }; }, sessionOptions); interface Props { - user: User; - groups: Group[]; - users: User[]; - mutateUser: Function; - linkedCorporate?: CorporateUser | MasterCorporateUser; + user: User; + hasBenefitsFromUniversity: boolean; + mutateUser: Function; + referralAgent?: User; + linkedCorporate?: CorporateUser | MasterCorporateUser; } -const DoubleColumnRow = ({ children }: { children: ReactNode }) =>
{children}
; +const DoubleColumnRow = ({ children }: { children: ReactNode }) => ( +
{children}
+); -function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props) { - const [bio, setBio] = useState(user.bio || ""); - const [name, setName] = useState(user.name || ""); - const [email, setEmail] = useState(user.email || ""); - const [password, setPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [profilePicture, setProfilePicture] = useState(user.profilePicture); +function UserProfile({ + user, + mutateUser, + linkedCorporate, + hasBenefitsFromUniversity, + referralAgent, +}: Props) { + const [bio, setBio] = useState(user.bio || ""); + const [name, setName] = useState(user.name || ""); + const [email, setEmail] = useState(user.email || ""); + const [password, setPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [profilePicture, setProfilePicture] = useState(user.profilePicture); - const [desiredLevels, setDesiredLevels] = useState(checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined); - const [focus, setFocus] = useState<"academic" | "general">(user.focus); + const [desiredLevels, setDesiredLevels] = useState( + checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined + ); + const [focus, setFocus] = useState<"academic" | "general">(user.focus); - const [country, setCountry] = useState(user.demographicInformation?.country || ""); - const [phone, setPhone] = useState(user.demographicInformation?.phone || ""); - const [gender, setGender] = useState(user.demographicInformation?.gender || undefined); - const [employment, setEmployment] = useState( - checkAccess(user, ["corporate", "mastercorporate"]) ? undefined : (user.demographicInformation as DemographicInformation)?.employment, - ); - const [passport_id, setPassportID] = useState( - checkAccess(user, ["student"]) ? (user.demographicInformation as DemographicInformation)?.passport_id : undefined, - ); + const [country, setCountry] = useState( + user.demographicInformation?.country || "" + ); + const [phone, setPhone] = useState( + user.demographicInformation?.phone || "" + ); + const [gender, setGender] = useState( + user.demographicInformation?.gender || undefined + ); + const [employment, setEmployment] = useState( + checkAccess(user, ["corporate", "mastercorporate"]) + ? undefined + : (user.demographicInformation as DemographicInformation)?.employment + ); + const [passport_id, setPassportID] = useState( + checkAccess(user, ["student"]) + ? (user.demographicInformation as DemographicInformation)?.passport_id + : undefined + ); - const [preferredGender, setPreferredGender] = useState( - user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined, - ); - const [preferredTopics, setPreferredTopics] = useState( - user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined, - ); + const [preferredGender, setPreferredGender] = useState< + InstructorGender | undefined + >( + user.type === "student" || user.type === "developer" + ? user.preferredGender || "varied" + : undefined + ); + const [preferredTopics, setPreferredTopics] = useState( + user.type === "student" || user.type === "developer" + ? user.preferredTopics + : undefined + ); - const [position, setPosition] = useState( - user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : undefined, - ); - const [corporateInformation, setCorporateInformation] = useState( - user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation : undefined, - ); + const [position, setPosition] = useState( + user.type === "corporate" || user.type === "mastercorporate" + ? user.demographicInformation?.position + : undefined + ); + const [corporateInformation, setCorporateInformation] = useState( + user.type === "corporate" || user.type === "mastercorporate" + ? user.corporateInformation + : undefined + ); - const [companyName] = useState(user.type === "agent" ? user.agentInformation?.companyName : undefined); - const [commercialRegistration] = useState(user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined); - const [arabName, setArabName] = useState(user.type === "agent" ? user.agentInformation?.companyArabName : undefined); - const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || moment.tz.guess()); - const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false); + const [companyName] = useState( + user.type === "agent" ? user.agentInformation?.companyName : undefined + ); + const [commercialRegistration] = useState( + user.type === "agent" + ? user.agentInformation?.commercialRegistration + : undefined + ); + const [arabName, setArabName] = useState( + user.type === "agent" ? user.agentInformation?.companyArabName : undefined + ); + const [timezone, setTimezone] = useState( + user.demographicInformation?.timezone || moment.tz.guess() + ); + const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false); - const profilePictureInput = useRef(null); - const expirationDateColor = (date: Date) => { - const momentDate = moment(date); - const today = moment(new Date()); + const profilePictureInput = useRef(null); + const expirationDateColor = (date: Date) => { + const momentDate = moment(date); + const today = moment(new Date()); - if (today.add(1, "days").isAfter(momentDate)) return "!bg-mti-red-ultralight border-mti-red-light"; - if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light"; - if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light"; - }; + if (today.add(1, "days").isAfter(momentDate)) + return "!bg-mti-red-ultralight border-mti-red-light"; + if (today.add(3, "days").isAfter(momentDate)) + return "!bg-mti-rose-ultralight border-mti-rose-light"; + if (today.add(7, "days").isAfter(momentDate)) + return "!bg-mti-orange-ultralight border-mti-orange-light"; + }; - const uploadProfilePicture = async (event: ChangeEvent) => { - if (event.target.files && event.target.files[0]) { - const picture = event.target.files[0]; - const base64 = await convertBase64(picture); - setProfilePicture(base64 as string); - } - }; + const uploadProfilePicture = async (event: ChangeEvent) => { + if (event.target.files && event.target.files[0]) { + const picture = event.target.files[0]; + const base64 = await convertBase64(picture); + setProfilePicture(base64 as string); + } + }; - const updateUser = async () => { - setIsLoading(true); - if (email !== user?.email && !password) { - toast.error("To update your e-mail you need to input your password!"); - setIsLoading(false); - return; - } + const updateUser = async () => { + setIsLoading(true); + if (email !== user?.email && !password) { + toast.error("To update your e-mail you need to input your password!"); + setIsLoading(false); + return; + } - if (newPassword && !password) { - toast.error("To update your password you need to input your current one!"); - setIsLoading(false); - return; - } + if (newPassword && !password) { + toast.error( + "To update your password you need to input your current one!" + ); + setIsLoading(false); + return; + } - if (email !== user?.email) { - const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin); - const message = - users.filter((x) => userAdmins.includes(x.id) && x.type === "corporate").length > 0 - ? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?" - : "Are you sure you want to update your e-mail address?"; + if (email !== user?.email) { + const message = hasBenefitsFromUniversity + ? "If you change your e-mail address, you will lose all benefits from your university/institute. Are you sure you want to continue?" + : "Are you sure you want to update your e-mail address?"; - if (!confirm(message)) { - setIsLoading(false); - return; - } - } + if (!confirm(message)) { + setIsLoading(false); + return; + } + } - axios - .post("/api/users/update", { - bio, - name, - email, - password, - newPassword, - profilePicture, - desiredLevels, - preferredGender, - preferredTopics, - focus, - demographicInformation: { - phone, - country, - employment: user?.type === "corporate" ? undefined : employment, - position: user?.type === "corporate" ? position : undefined, - gender, - passport_id, - timezone, - }, - ...(user.type === "corporate" ? { corporateInformation } : {}), - ...(user.type === "agent" - ? { - agentInformation: { - companyName, - commercialRegistration, - arabName, - }, - } - : {}), - }) - .then((response) => { - if (response.status === 200) { - toast.success("Your profile has been updated!"); - mutateUser((response.data as { user: User }).user); - setIsLoading(false); - return; - } - }) - .catch((error) => { - console.log(error); - toast.error((error.response.data as ErrorMessage).message); - }) - .finally(() => { - setIsLoading(false); - }); - }; + axios + .post("/api/users/update", { + bio, + name, + email, + password, + newPassword, + profilePicture, + desiredLevels, + preferredGender, + preferredTopics, + focus, + demographicInformation: { + phone, + country, + employment: user?.type === "corporate" ? undefined : employment, + position: user?.type === "corporate" ? position : undefined, + gender, + passport_id, + timezone, + }, + ...(user.type === "corporate" ? { corporateInformation } : {}), + ...(user.type === "agent" + ? { + agentInformation: { + companyName, + commercialRegistration, + arabName, + }, + } + : {}), + }) + .then((response) => { + if (response.status === 200) { + toast.success("Your profile has been updated!"); + mutateUser((response.data as { user: User }).user); + setIsLoading(false); + return; + } + }) + .catch((error) => { + console.log(error); + toast.error((error.response.data as ErrorMessage).message); + }) + .finally(() => { + setIsLoading(false); + }); + }; - const ExpirationDate = () => ( -
- - - {!user.subscriptionExpirationDate && "Unlimited"} - {user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} - -
- ); + const ExpirationDate = () => ( +
+ + + {!user.subscriptionExpirationDate && "Unlimited"} + {user.subscriptionExpirationDate && + moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} + +
+ ); - const TimezoneInput = () => ( -
- - -
- ); + const TimezoneInput = () => ( +
+ + +
+ ); - const manualDownloadLink = ["student", "teacher", "corporate"].includes(user.type) ? `/manuals/${user.type}.pdf` : ""; + const manualDownloadLink = ["student", "teacher", "corporate"].includes( + user.type + ) + ? `/manuals/${user.type}.pdf` + : ""; - return ( - -
-

Edit Profile

-
-
-

Edit Profile

-
e.preventDefault()}> - - {user.type !== "corporate" && user.type !== "mastercorporate" && ( - setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> - )} + return ( + <> +
+

Edit Profile

+
+
+

Edit Profile

+ e.preventDefault()} + > + + {user.type !== "corporate" && + user.type !== "mastercorporate" && ( + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> + )} - {user.type === "agent" && ( - setArabName(e)} - placeholder="Enter your arab name" - defaultValue={arabName} - required - /> - )} + {user.type === "agent" && ( + setArabName(e)} + placeholder="Enter your arab name" + defaultValue={arabName} + required + /> + )} - setEmail(e)} - placeholder="Enter email address" - defaultValue={email} - required - /> - - - setPassword(e)} - placeholder="Enter your password" - required - /> - setNewPassword(e)} - placeholder="Enter your new password (optional)" - /> - - {user.type === "agent" && ( -
- null} - placeholder="Enter your company's name" - defaultValue={companyName} - disabled - /> - null} - placeholder="Enter commercial registration" - defaultValue={commercialRegistration} - disabled - /> -
- )} + setEmail(e)} + placeholder="Enter email address" + defaultValue={email} + required + /> + + + setPassword(e)} + placeholder="Enter your password" + required + /> + setNewPassword(e)} + placeholder="Enter your new password (optional)" + /> + + {user.type === "agent" && ( +
+ null} + placeholder="Enter your company's name" + defaultValue={companyName} + disabled + /> + null} + placeholder="Enter commercial registration" + defaultValue={commercialRegistration} + disabled + /> +
+ )} - -
- - -
- setPhone(e)} - placeholder="Enter phone number" - defaultValue={phone} - required - /> -
+ +
+ + +
+ setPhone(e)} + placeholder="Enter phone number" + defaultValue={phone} + required + /> +
- {user.type === "student" ? ( - - setPassportID(e)} - placeholder="Enter National ID or Passport number" - value={passport_id} - required - /> - - - ) : ( - - )} + {user.type === "student" ? ( + + setPassportID(e)} + placeholder="Enter National ID or Passport number" + value={passport_id} + required + /> + + + ) : ( + + )} - + - {desiredLevels && ["developer", "student"].includes(user.type) && ( - <> -
- - >} - /> -
-
- -
- - -
-
- - )} + {desiredLevels && + ["developer", "student"].includes(user.type) && ( + <> +
+ + + > + } + /> +
+
+ +
+ + +
+
+ + )} - {preferredGender && ["developer", "student"].includes(user.type) && ( - <> - - -
- - + value + ? setPreferredGender( + value.value as InstructorGender + ) + : null + } + options={[ + { value: "male", label: "Male" }, + { value: "female", label: "Female" }, + { value: "varied", label: "Varied" }, + ]} + /> +
+
+ + +
+
- setIsPreferredTopicsOpen(false)} - selectTopics={setPreferredTopics} - initialTopics={preferredTopics || []} - /> + setIsPreferredTopicsOpen(false)} + selectTopics={setPreferredTopics} + initialTopics={preferredTopics || []} + /> - - - )} + + + )} - {user.type === "corporate" && ( - <> - - null} - label="Pricing" - defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} - disabled - required - /> - - - - )} + {user.type === "corporate" && ( + <> + + null} + label="Pricing" + defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} + disabled + required + /> + + + + )} - {user.type === "corporate" && ( - <> - - - setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> - - - - )} + {user.type === "corporate" && ( + <> + + + setName(e)} + placeholder="Enter your name" + defaultValue={name} + required + /> + + + + )} - {user.type === "corporate" && user.corporateInformation.referralAgent && ( - <> - - - null} - defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name} - type="text" - label="Country Manager's Name" - placeholder="Not available" - required - disabled - /> - null} - defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email} - type="text" - label="Country Manager's E-mail" - placeholder="Not available" - required - disabled - /> - - -
- - x.id === user.corporateInformation.referralAgent)?.demographicInformation - ?.country - } - onChange={() => null} - disabled - /> -
+ {user.type === "corporate" && + user.corporateInformation.referralAgent && + referralAgent && ( + <> + + + null} + defaultValue={referralAgent?.name} + type="text" + label="Country Manager's Name" + placeholder="Not available" + required + disabled + /> + null} + defaultValue={referralAgent?.email} + type="text" + label="Country Manager's E-mail" + placeholder="Not available" + required + disabled + /> + + +
+ + null} + disabled + /> +
- null} - placeholder="Not available" - defaultValue={ - users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone - } - disabled - required - /> -
- - )} + null} + placeholder="Not available" + defaultValue={ + referralAgent?.demographicInformation?.phone + } + disabled + required + /> +
+ + )} - {user.type !== "corporate" && ( - - + {user.type !== "corporate" && ( + + -
- - -
-
- )} - -
-
-
(profilePictureInput.current as any)?.click()}> -
-
- -
- {user.name} -
- - (profilePictureInput.current as any)?.click()} - className="cursor-pointer text-mti-purple-light text-sm"> - Change picture - -
{USER_TYPE_LABELS[user.type]}
-
- {user.type === "agent" && ( -
- {user.demographicInformation?.country.toLowerCase() -
- )} - {manualDownloadLink && ( - - - - )} -
-
-
- Bio -