ENCOA-316 ENCOA-317:
Refactor components to remove Layout wrapper and pass it in the App component , implemented a skeleton feedback while loading page and improved API calls related to Dashboard/User Profile
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import useEntities from "@/hooks/useEntities";
|
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -6,34 +5,92 @@ import { useRouter } from "next/router";
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Navbar from "../Navbar";
|
import Navbar from "../Navbar";
|
||||||
import Sidebar from "../Sidebar";
|
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<React.SetStateAction<boolean>>,
|
||||||
|
focusMode: false,
|
||||||
|
setFocusMode: (() => {}) as React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
hideSidebar: false,
|
||||||
|
setHideSidebar: (() => {}) as React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
bgColor: "bg-white",
|
||||||
|
setBgColor: (() => {}) as React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
className: "",
|
||||||
|
setClassName: (() => {}) as React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
entities?: EntityWithRoles[]
|
entities?: EntityWithRoles[];
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
refreshPage?: boolean;
|
||||||
navDisabled?: boolean;
|
|
||||||
focusMode?: boolean;
|
|
||||||
hideSidebar?: boolean
|
|
||||||
bgColor?: string;
|
|
||||||
onFocusLayerMouseEnter?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({
|
export default function Layout({
|
||||||
user,
|
user,
|
||||||
|
entities,
|
||||||
children,
|
children,
|
||||||
className,
|
refreshPage,
|
||||||
bgColor = "bg-white",
|
|
||||||
hideSidebar,
|
|
||||||
navDisabled = false,
|
|
||||||
focusMode = false,
|
|
||||||
onFocusLayerMouseEnter
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
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("");
|
||||||
|
|
||||||
|
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();
|
const router = useRouter();
|
||||||
const { entities } = useEntities()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={clsx("w-full min-h-full h-screen flex flex-col bg-mti-gray-smoke relative")}>
|
<LayoutContext.Provider value={LayoutContextValue}>
|
||||||
|
<main
|
||||||
|
className={clsx(
|
||||||
|
"w-full min-h-full h-screen flex flex-col bg-mti-gray-smoke relative"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{!hideSidebar && user && (
|
{!hideSidebar && user && (
|
||||||
<Navbar
|
<Navbar
|
||||||
@@ -61,11 +118,13 @@ export default function Layout({
|
|||||||
`w-full min-h-full ${bgColor} shadow-md rounded-2xl p-4 xl:p-10 pb-8 flex flex-col gap-8 relative overflow-hidden mt-2`,
|
`w-full min-h-full ${bgColor} shadow-md rounded-2xl p-4 xl:p-10 pb-8 flex flex-col gap-8 relative overflow-hidden mt-2`,
|
||||||
bgColor !== "bg-white" ? "justify-center" : "h-fit",
|
bgColor !== "bg-white" ? "justify-center" : "h-fit",
|
||||||
hideSidebar ? "md:mx-8" : "md:mr-8",
|
hideSidebar ? "md:mx-8" : "md:mr-8",
|
||||||
className,
|
className
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</LayoutContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ export default function AsyncSelect({
|
|||||||
flat,
|
flat,
|
||||||
}: Props & (MultiProps | SingleProps)) {
|
}: Props & (MultiProps | SingleProps)) {
|
||||||
const [target, setTarget] = useState<HTMLElement>();
|
const [target, setTarget] = useState<HTMLElement>();
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (document) setTarget(document.body);
|
if (document) setTarget(document.body);
|
||||||
@@ -77,7 +86,7 @@ export default function AsyncSelect({
|
|||||||
filterOption={null}
|
filterOption={null}
|
||||||
loadingMessage={() => "Loading..."}
|
loadingMessage={() => "Loading..."}
|
||||||
onInputChange={(inputValue) => {
|
onInputChange={(inputValue) => {
|
||||||
loadOptions(inputValue);
|
setInputValue(inputValue);
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
60
src/components/Medium/UserProfileSkeleton.tsx
Normal file
60
src/components/Medium/UserProfileSkeleton.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function UserProfileSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white min-h-screen p-6">
|
||||||
|
<div className="mt-6 bg-white p-6 rounded-lg flex gap-4 items-center">
|
||||||
|
<div className="h-64 w-60 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="h-12 w-64 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="flex justify-between items-center mt-1">
|
||||||
|
<div className="h-4 w-60 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
<div className="h-8 w-32 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-100 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
<div className="mt-6 grid grid-cols-4 justify-item-start gap-4">
|
||||||
|
<div className="bg-white p-4 rounded-lg text-center flex flex-row items-center justify-center">
|
||||||
|
<div className="h-12 w-12 mx-2 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="h-4 w-4 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
<div className="h-4 w-16 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg text-center flex flex-row items-center justify-center">
|
||||||
|
<div className="h-12 w-12 mx-2 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="h-4 w-4 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
<div className="h-4 w-16 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg text-center flex flex-row items-center justify-center">
|
||||||
|
<div className="h-12 w-12 mx-2 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="h-4 w-4 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
<div className="h-4 w-16 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg text-center flex flex-row items-center justify-center">
|
||||||
|
<div className="h-12 w-12 mx-2 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="h-4 w-4 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
<div className="h-4 w-16 bg-gray-300 animate-pulse mt-2 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 bg-white p-6 rounded-lg">
|
||||||
|
<div className="h-6 w-40 bg-gray-300 animate-pulse rounded mb-4"></div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="flex justify-between items-center">
|
||||||
|
<div className="h-4 w-24 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
<div className="h-2 w-3/4 bg-gray-300 animate-pulse rounded"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import { MdSpaceDashboard } from "react-icons/md";
|
|||||||
import {
|
import {
|
||||||
BsFileEarmarkText,
|
BsFileEarmarkText,
|
||||||
BsClockHistory,
|
BsClockHistory,
|
||||||
BsPencil,
|
|
||||||
BsGraphUp,
|
BsGraphUp,
|
||||||
BsChevronBarRight,
|
BsChevronBarRight,
|
||||||
BsChevronBarLeft,
|
BsChevronBarLeft,
|
||||||
@@ -24,11 +23,15 @@ import { preventNavigation } from "@/utils/navigation.disabled";
|
|||||||
import usePreferencesStore from "@/stores/preferencesStore";
|
import usePreferencesStore from "@/stores/preferencesStore";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import useTicketsListener from "@/hooks/useTicketsListener";
|
import useTicketsListener from "@/hooks/useTicketsListener";
|
||||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
import { getTypesOfUser } from "@/utils/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { useAllowedEntities, useAllowedEntitiesSomePermissions } from "@/hooks/useEntityPermissions";
|
import {
|
||||||
|
useAllowedEntities,
|
||||||
|
useAllowedEntitiesSomePermissions,
|
||||||
|
} from "@/hooks/useEntityPermissions";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { PermissionType } from "../interfaces/permissions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -37,7 +40,7 @@ interface Props {
|
|||||||
onFocusLayerMouseEnter?: () => void;
|
onFocusLayerMouseEnter?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
user: User;
|
user: User;
|
||||||
entities?: EntityWithRoles[]
|
entities?: EntityWithRoles[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavProps {
|
interface NavProps {
|
||||||
@@ -50,17 +53,28 @@ interface NavProps {
|
|||||||
badge?: number;
|
badge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Nav = ({ Icon, label, path, keyPath, disabled = false, isMinimized = false, badge }: NavProps) => {
|
const Nav = ({
|
||||||
|
Icon,
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
keyPath,
|
||||||
|
disabled = false,
|
||||||
|
isMinimized = false,
|
||||||
|
badge,
|
||||||
|
}: NavProps) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={!disabled ? keyPath : ""}
|
href={!disabled ? keyPath : ""}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
|
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
|
||||||
"transition-all duration-300 ease-in-out relative",
|
"transition-all duration-300 ease-in-out relative",
|
||||||
disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer",
|
disabled
|
||||||
|
? "hover:bg-mti-gray-dim cursor-not-allowed"
|
||||||
|
: "hover:bg-mti-purple-light cursor-pointer",
|
||||||
path.startsWith(keyPath) && "bg-mti-purple-light text-white",
|
path.startsWith(keyPath) && "bg-mti-purple-light text-white",
|
||||||
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]",
|
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<Icon size={24} />
|
<Icon size={24} />
|
||||||
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
|
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
|
||||||
{!!badge && badge > 0 && (
|
{!!badge && badge > 0 && (
|
||||||
@@ -68,8 +82,9 @@ const Nav = ({ Icon, label, path, keyPath, disabled = false, isMinimized = false
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
|
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300",
|
||||||
isMinimized && "absolute right-0 top-0",
|
isMinimized && "absolute right-0 top-0"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{badge}
|
{badge}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -84,23 +99,154 @@ export default function Sidebar({
|
|||||||
focusMode = false,
|
focusMode = false,
|
||||||
user,
|
user,
|
||||||
onFocusLayerMouseEnter,
|
onFocusLayerMouseEnter,
|
||||||
className
|
className,
|
||||||
}: Props) {
|
}: 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 entitiesAllowStatistics = useAllowedEntities(
|
||||||
const entitiesAllowPaymentRecord = useAllowedEntities(user, entities, "view_payment_record")
|
user,
|
||||||
|
entities,
|
||||||
|
"view_statistics"
|
||||||
|
);
|
||||||
|
const entitiesAllowPaymentRecord = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"view_payment_record"
|
||||||
|
);
|
||||||
|
|
||||||
const entitiesAllowGeneration = useAllowedEntitiesSomePermissions(user, entities, [
|
const entitiesAllowGeneration = useAllowedEntitiesSomePermissions(
|
||||||
"generate_reading", "generate_listening", "generate_writing", "generate_speaking", "generate_level"
|
user,
|
||||||
])
|
entities,
|
||||||
|
[
|
||||||
|
"generate_reading",
|
||||||
|
"generate_listening",
|
||||||
|
"generate_writing",
|
||||||
|
"generate_speaking",
|
||||||
|
"generate_level",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user || !user?.type) return sidebarPermissions;
|
||||||
|
|
||||||
|
const neededPermissions = permissions.reduce((acc, curr) => {
|
||||||
|
if (
|
||||||
|
["viewExams", "viewRecords", "viewTickets"].includes(curr as string)
|
||||||
|
) {
|
||||||
|
acc.push(curr);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as PermissionType[]);
|
||||||
|
|
||||||
|
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 () => {
|
const logout = async () => {
|
||||||
axios.post("/api/logout").finally(() => {
|
axios.post("/api/logout").finally(() => {
|
||||||
@@ -115,17 +261,39 @@ export default function Sidebar({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8",
|
"relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8",
|
||||||
isMinimized ? "w-fit" : "-xl:w-fit w-1/6",
|
isMinimized ? "w-fit" : "-xl:w-fit w-1/6",
|
||||||
className,
|
className
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<div className="-xl:hidden flex-col gap-3 xl:flex">
|
<div className="-xl:hidden flex-col gap-3 xl:flex">
|
||||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/dashboard" isMinimized={isMinimized} />
|
<Nav
|
||||||
{checkAccess(user, ["student", "teacher", "developer"], permissions, "viewExams") && (
|
disabled={disableNavigation}
|
||||||
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Practice" path={path} keyPath="/exam" isMinimized={isMinimized} />
|
Icon={MdSpaceDashboard}
|
||||||
|
label="Dashboard"
|
||||||
|
path={path}
|
||||||
|
keyPath="/dashboard"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
|
{sidebarPermissions["viewExams"] && (
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsFileEarmarkText}
|
||||||
|
label="Practice"
|
||||||
|
path={path}
|
||||||
|
keyPath="/exam"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"])) && entitiesAllowStatistics.length > 0 && (
|
{sidebarPermissions["viewStats"] && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsGraphUp}
|
||||||
|
label="Stats"
|
||||||
|
path={path}
|
||||||
|
keyPath="/stats"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["developer", "admin", "mastercorporate", "corporate", "teacher", "student"], permissions) && (
|
{sidebarPermissions["viewClassrooms"] && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsPeople}
|
Icon={BsPeople}
|
||||||
@@ -135,13 +303,27 @@ export default function Sidebar({
|
|||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
{sidebarPermissions["viewRecords"] && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsClockHistory}
|
||||||
|
label="Record"
|
||||||
|
path={path}
|
||||||
|
keyPath="/record"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
{sidebarPermissions["viewRecords"] && (
|
||||||
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={isMinimized} />
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={CiDumbbell}
|
||||||
|
label="Training"
|
||||||
|
path={path}
|
||||||
|
keyPath="/training"
|
||||||
|
isMinimized={isMinimized}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["admin", "developer", "agent", "corporate", "mastercorporate"]) && entitiesAllowPaymentRecord.length > 0 && (
|
{sidebarPermissions["viewPaymentRecords"] && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCurrencyDollar}
|
Icon={BsCurrencyDollar}
|
||||||
@@ -151,7 +333,7 @@ export default function Sidebar({
|
|||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"]) && (
|
{sidebarPermissions["viewSettings"] && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsShieldFill}
|
Icon={BsShieldFill}
|
||||||
@@ -161,7 +343,7 @@ export default function Sidebar({
|
|||||||
isMinimized={isMinimized}
|
isMinimized={isMinimized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["admin", "developer", "agent"], permissions, "viewTickets") && (
|
{sidebarPermissions["viewTickets"] && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsClipboardData}
|
Icon={BsClipboardData}
|
||||||
@@ -172,8 +354,7 @@ export default function Sidebar({
|
|||||||
badge={totalAssignedTickets}
|
badge={totalAssignedTickets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, ["admin", "developer", "teacher", 'corporate', 'mastercorporate'])
|
{sidebarPermissions["viewGeneration"] && (
|
||||||
&& (entitiesAllowGeneration.length > 0 || isAdmin) && (
|
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCloudFill}
|
Icon={BsCloudFill}
|
||||||
@@ -185,21 +366,63 @@ export default function Sidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
||||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized />
|
<Nav
|
||||||
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized />
|
disabled={disableNavigation}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
|
Icon={MdSpaceDashboard}
|
||||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized />
|
label="Dashboard"
|
||||||
|
path={path}
|
||||||
|
keyPath="/"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsFileEarmarkText}
|
||||||
|
label="Exams"
|
||||||
|
path={path}
|
||||||
|
keyPath="/exam"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
|
{sidebarPermissions["viewStats"] && (
|
||||||
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsGraphUp}
|
||||||
|
label="Stats"
|
||||||
|
path={path}
|
||||||
|
keyPath="/stats"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
{sidebarPermissions["viewRecords"] && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized />
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsClockHistory}
|
||||||
|
label="Record"
|
||||||
|
path={path}
|
||||||
|
keyPath="/record"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
|
{sidebarPermissions["viewRecords"] && (
|
||||||
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized />
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={CiDumbbell}
|
||||||
|
label="Training"
|
||||||
|
path={path}
|
||||||
|
keyPath="/training"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{checkAccess(user, getTypesOfUser(["student"])) && (
|
{sidebarPermissions["viewSettings"] && (
|
||||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized />
|
<Nav
|
||||||
|
disabled={disableNavigation}
|
||||||
|
Icon={BsShieldFill}
|
||||||
|
label="Settings"
|
||||||
|
path={path}
|
||||||
|
keyPath="/settings"
|
||||||
|
isMinimized
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{entitiesAllowGeneration.length > 0 && (
|
{sidebarPermissions["viewGeneration"] && (
|
||||||
<Nav
|
<Nav
|
||||||
disabled={disableNavigation}
|
disabled={disableNavigation}
|
||||||
Icon={BsCloudFill}
|
Icon={BsCloudFill}
|
||||||
@@ -218,10 +441,17 @@ export default function Sidebar({
|
|||||||
onClick={toggleMinimize}
|
onClick={toggleMinimize}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"hover:text-mti-rose -xl:hidden flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
|
"hover:text-mti-rose -xl:hidden 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 ? "w-fit" : "w-full min-w-[250px] px-8"
|
||||||
)}>
|
)}
|
||||||
{isMinimized ? <BsChevronBarRight size={24} /> : <BsChevronBarLeft size={24} />}
|
>
|
||||||
{!isMinimized && <span className="text-lg font-medium">Minimize</span>}
|
{isMinimized ? (
|
||||||
|
<BsChevronBarRight size={24} />
|
||||||
|
) : (
|
||||||
|
<BsChevronBarLeft size={24} />
|
||||||
|
)}
|
||||||
|
{!isMinimized && (
|
||||||
|
<span className="text-lg font-medium">Minimize</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
@@ -229,13 +459,18 @@ export default function Sidebar({
|
|||||||
onClick={focusMode ? () => {} : logout}
|
onClick={focusMode ? () => {} : logout}
|
||||||
className={clsx(
|
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",
|
"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 ? "w-fit" : "w-full min-w-[250px] px-8"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<RiLogoutBoxFill size={24} />
|
<RiLogoutBoxFill size={24} />
|
||||||
{!isMinimized && <span className="-xl:hidden text-lg font-medium">Log Out</span>}
|
{!isMinimized && (
|
||||||
|
<span className="-xl:hidden text-lg font-medium">Log Out</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{focusMode && <FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />}
|
{focusMode && (
|
||||||
|
<FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
BsPen,
|
BsPen,
|
||||||
BsXCircle,
|
BsXCircle,
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import { totalExamsByModule } from "@/utils/stats";
|
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import { sortByModuleName } from "@/utils/moduleUtils";
|
import { sortByModuleName } from "@/utils/moduleUtils";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
@@ -24,7 +22,7 @@ import { Variant } from "@/interfaces/exam";
|
|||||||
import useSessions, { Session } from "@/hooks/useSessions";
|
import useSessions, { Session } from "@/hooks/useSessions";
|
||||||
import SessionCard from "@/components/Medium/SessionCard";
|
import SessionCard from "@/components/Medium/SessionCard";
|
||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import moment from "moment";
|
import useStats from "../hooks/useStats";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -41,7 +39,21 @@ export default function Selection({ user, page, onStart }: Props) {
|
|||||||
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
|
|
||||||
const { data: stats } = useFilterRecordsByUser<Stat[]>(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<Module, number>;
|
||||||
|
}>(user?.id, !user?.id, "byModule");
|
||||||
const { sessions, isLoading, reload } = useSessions(user.id);
|
const { sessions, isLoading, reload } = useSessions(user.id);
|
||||||
|
|
||||||
const dispatch = useExamStore((state) => state.dispatch);
|
const dispatch = useExamStore((state) => state.dispatch);
|
||||||
@@ -77,7 +89,7 @@ export default function Selection({ user, page, onStart }: Props) {
|
|||||||
<BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />
|
<BsBook className="text-ielts-reading h-6 w-6 md:h-8 md:w-8" />
|
||||||
),
|
),
|
||||||
label: "Reading",
|
label: "Reading",
|
||||||
value: totalExamsByModule(stats, "reading"),
|
value: reading,
|
||||||
tooltip: "The amount of reading exams performed.",
|
tooltip: "The amount of reading exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,7 +97,7 @@ export default function Selection({ user, page, onStart }: Props) {
|
|||||||
<BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />
|
<BsHeadphones className="text-ielts-listening h-6 w-6 md:h-8 md:w-8" />
|
||||||
),
|
),
|
||||||
label: "Listening",
|
label: "Listening",
|
||||||
value: totalExamsByModule(stats, "listening"),
|
value: listening,
|
||||||
tooltip: "The amount of listening exams performed.",
|
tooltip: "The amount of listening exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -93,7 +105,7 @@ export default function Selection({ user, page, onStart }: Props) {
|
|||||||
<BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />
|
<BsPen className="text-ielts-writing h-6 w-6 md:h-8 md:w-8" />
|
||||||
),
|
),
|
||||||
label: "Writing",
|
label: "Writing",
|
||||||
value: totalExamsByModule(stats, "writing"),
|
value: writing,
|
||||||
tooltip: "The amount of writing exams performed.",
|
tooltip: "The amount of writing exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,7 +113,7 @@ export default function Selection({ user, page, onStart }: Props) {
|
|||||||
<BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />
|
<BsMegaphone className="text-ielts-speaking h-6 w-6 md:h-8 md:w-8" />
|
||||||
),
|
),
|
||||||
label: "Speaking",
|
label: "Speaking",
|
||||||
value: totalExamsByModule(stats, "speaking"),
|
value: speaking,
|
||||||
tooltip: "The amount of speaking exams performed.",
|
tooltip: "The amount of speaking exams performed.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,7 +121,7 @@ export default function Selection({ user, page, onStart }: Props) {
|
|||||||
<BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />
|
<BsClipboard className="text-ielts-level h-6 w-6 md:h-8 md:w-8" />
|
||||||
),
|
),
|
||||||
label: "Level",
|
label: "Level",
|
||||||
value: totalExamsByModule(stats, "level"),
|
value: level,
|
||||||
tooltip: "The amount of level exams performed.",
|
tooltip: "The amount of level exams performed.",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Discount } from "@/interfaces/paypal";
|
|
||||||
import { Code, Group, User } from "@/interfaces/user";
|
|
||||||
import axios from "axios";
|
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<EntityWithRoles[]>([]);
|
const [entities, setEntities] = useState<EntityWithRoles[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
const getData = () => {
|
const getData = useCallback(() => {
|
||||||
|
if (shouldNot) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
axios
|
axios
|
||||||
.get<EntityWithRoles[]>("/api/entities?showRoles=true")
|
.get<EntityWithRoles[]>("/api/entities?showRoles=true")
|
||||||
.then((response) => setEntities(response.data))
|
.then((response) => setEntities(response.data))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
}, [shouldNot]);
|
||||||
|
|
||||||
useEffect(getData, []);
|
useEffect(getData, [getData])
|
||||||
|
|
||||||
return { entities, isLoading, isError, reload: getData };
|
return { entities, isLoading, isError, reload: getData };
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/hooks/useStats.tsx
Normal file
42
src/hooks/useStats.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function useStats<T extends any>(
|
||||||
|
id?: string,
|
||||||
|
shouldNotQuery: boolean = !id,
|
||||||
|
queryType: string = "stats"
|
||||||
|
) {
|
||||||
|
type ElementType = T extends (infer U)[] ? U : never;
|
||||||
|
|
||||||
|
const [data, setData] = useState<T>({} 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<T>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const useTicketsListener = (userId?: string) => {
|
const useTicketsListener = (userId?: string, canFetch?: boolean) => {
|
||||||
const [assignedTickets, setAssignedTickets] = useState([]);
|
const [assignedTickets, setAssignedTickets] = useState([]);
|
||||||
|
|
||||||
const getData = () => {
|
const getData = useCallback(() => {
|
||||||
axios
|
axios
|
||||||
.get("/api/tickets/assignedToUser")
|
.get("/api/tickets/assignedToUser")
|
||||||
.then((response) => setAssignedTickets(response.data));
|
.then((response) => setAssignedTickets(response.data));
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getData();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!canFetch) return;
|
||||||
|
getData();
|
||||||
|
}, [canFetch, getData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canFetch) return;
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
getData();
|
getData();
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [assignedTickets]);
|
}, [assignedTickets, canFetch, getData]);
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,3 +37,5 @@ export interface Assignment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AssignmentWithCorporateId = Assignment & { corporateId: string };
|
export type AssignmentWithCorporateId = Assignment & { corporateId: string };
|
||||||
|
|
||||||
|
export type AssignmentWithHasResults = Assignment & { hasResults: boolean };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import { Module } from "@/interfaces";
|
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 AbandonPopup from "@/components/AbandonPopup";
|
||||||
import Layout from "@/components/High/Layout";
|
import { LayoutContext } from "@/components/High/Layout";
|
||||||
import Finish from "@/exams/Finish";
|
import Finish from "@/exams/Finish";
|
||||||
import Level from "@/exams/Level";
|
import Level from "@/exams/Level";
|
||||||
import Listening from "@/exams/Listening";
|
import Listening from "@/exams/Listening";
|
||||||
@@ -11,9 +11,12 @@ import Reading from "@/exams/Reading";
|
|||||||
import Selection from "@/exams/Selection";
|
import Selection from "@/exams/Selection";
|
||||||
import Speaking from "@/exams/Speaking";
|
import Speaking from "@/exams/Speaking";
|
||||||
import Writing from "@/exams/Writing";
|
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 { User } from "@/interfaces/user";
|
||||||
import { evaluateSpeakingAnswer, evaluateWritingAnswer } from "@/utils/evaluation";
|
import {
|
||||||
|
evaluateSpeakingAnswer,
|
||||||
|
evaluateWritingAnswer,
|
||||||
|
} from "@/utils/evaluation";
|
||||||
import { getExam } from "@/utils/exams";
|
import { getExam } from "@/utils/exams";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -26,11 +29,16 @@ import useEvaluationPolling from "@/hooks/useEvaluationPolling";
|
|||||||
interface Props {
|
interface Props {
|
||||||
page: "exams" | "exercises";
|
page: "exams" | "exercises";
|
||||||
user: User;
|
user: User;
|
||||||
destination?: string
|
destination?: string;
|
||||||
hideSidebar?: boolean
|
hideSidebar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExamPage({ page, user, destination = "/", hideSidebar = false }: Props) {
|
export default function ExamPage({
|
||||||
|
page,
|
||||||
|
user,
|
||||||
|
destination = "/",
|
||||||
|
hideSidebar = false,
|
||||||
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
||||||
@@ -38,14 +46,22 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
const [pendingExercises, setPendingExercises] = useState<string[]>([]);
|
const [pendingExercises, setPendingExercises] = useState<string[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
exam, setExam,
|
exam,
|
||||||
|
setExam,
|
||||||
exams,
|
exams,
|
||||||
sessionId, setSessionId, setPartIndex,
|
sessionId,
|
||||||
moduleIndex, setModuleIndex,
|
setSessionId,
|
||||||
setQuestionIndex, setExerciseIndex,
|
setPartIndex,
|
||||||
userSolutions, setUserSolutions,
|
moduleIndex,
|
||||||
showSolutions, setShowSolutions,
|
setModuleIndex,
|
||||||
selectedModules, setSelectedModules,
|
setQuestionIndex,
|
||||||
|
setExerciseIndex,
|
||||||
|
userSolutions,
|
||||||
|
setUserSolutions,
|
||||||
|
showSolutions,
|
||||||
|
setShowSolutions,
|
||||||
|
selectedModules,
|
||||||
|
setSelectedModules,
|
||||||
setUser,
|
setUser,
|
||||||
inactivity,
|
inactivity,
|
||||||
timeSpent,
|
timeSpent,
|
||||||
@@ -62,7 +78,9 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
} = useExamStore();
|
} = useExamStore();
|
||||||
|
|
||||||
const [isFetchingExams, setIsFetchingExams] = useState(false);
|
const [isFetchingExams, setIsFetchingExams] = useState(false);
|
||||||
const [isExamLoaded, setIsExamLoaded] = useState(moduleIndex < selectedModules.length);
|
const [isExamLoaded, setIsExamLoaded] = useState(
|
||||||
|
moduleIndex < selectedModules.length
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsExamLoaded(moduleIndex < selectedModules.length);
|
setIsExamLoaded(moduleIndex < selectedModules.length);
|
||||||
@@ -89,13 +107,21 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
module,
|
module,
|
||||||
avoidRepeated,
|
avoidRepeated,
|
||||||
variant,
|
variant,
|
||||||
user?.type === "student" || user?.type === "developer" ? user.preferredGender : undefined,
|
user?.type === "student" || user?.type === "developer"
|
||||||
),
|
? user.preferredGender
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
);
|
);
|
||||||
Promise.all(examPromises).then((values) => {
|
Promise.all(examPromises).then((values) => {
|
||||||
setIsFetchingExams(false);
|
setIsFetchingExams(false);
|
||||||
if (values.every((x) => !!x)) {
|
if (values.every((x) => !!x)) {
|
||||||
dispatch({ type: 'INIT_EXAM', payload: { exams: values.map((x) => x!), modules: selectedModules } })
|
dispatch({
|
||||||
|
type: "INIT_EXAM",
|
||||||
|
payload: {
|
||||||
|
exams: values.map((x) => x!),
|
||||||
|
modules: selectedModules,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error("Something went wrong, please try again");
|
toast.error("Something went wrong, please try again");
|
||||||
setTimeout(router.reload, 500);
|
setTimeout(router.reload, 500);
|
||||||
@@ -106,7 +132,6 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedModules, exams]);
|
}, [selectedModules, exams]);
|
||||||
|
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
resetStore();
|
resetStore();
|
||||||
setVariant("full");
|
setVariant("full");
|
||||||
@@ -116,28 +141,44 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeModule && !showSolutions && flags.pendingEvaluation) {
|
if (flags.finalizeModule && !showSolutions && flags.pendingEvaluation) {
|
||||||
if (exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0 && !showSolutions) {
|
if (
|
||||||
const exercisesToEvaluate = exam.exercises
|
exam &&
|
||||||
.map(exercise => exercise.id);
|
(exam.module === "writing" || exam.module === "speaking") &&
|
||||||
|
userSolutions.length > 0 &&
|
||||||
|
!showSolutions
|
||||||
|
) {
|
||||||
|
const exercisesToEvaluate = exam.exercises.map(
|
||||||
|
(exercise) => exercise.id
|
||||||
|
);
|
||||||
|
|
||||||
setPendingExercises(exercisesToEvaluate);
|
setPendingExercises(exercisesToEvaluate);
|
||||||
(async () => {
|
(async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
exam.exercises.map(async (exercise, index) => {
|
exam.exercises.map(async (exercise, index) => {
|
||||||
if (exercise.type === "writing")
|
if (exercise.type === "writing")
|
||||||
await evaluateWritingAnswer(user.id, sessionId, exercise, index + 1, userSolutions.find((x) => x.exercise === exercise.id)!, exercise.attachment?.url);
|
await evaluateWritingAnswer(
|
||||||
|
user.id,
|
||||||
|
sessionId,
|
||||||
|
exercise,
|
||||||
|
index + 1,
|
||||||
|
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||||
|
exercise.attachment?.url
|
||||||
|
);
|
||||||
|
|
||||||
if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") {
|
if (
|
||||||
|
exercise.type === "interactiveSpeaking" ||
|
||||||
|
exercise.type === "speaking"
|
||||||
|
) {
|
||||||
await evaluateSpeakingAnswer(
|
await evaluateSpeakingAnswer(
|
||||||
user.id,
|
user.id,
|
||||||
sessionId,
|
sessionId,
|
||||||
exercise,
|
exercise,
|
||||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||||
index + 1,
|
index + 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,18 +189,22 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeExam && moduleIndex !== -1) {
|
if (flags.finalizeExam && moduleIndex !== -1) {
|
||||||
setModuleIndex(-1);
|
setModuleIndex(-1);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [flags.finalizeExam, moduleIndex, setModuleIndex]);
|
}, [flags.finalizeExam, moduleIndex, setModuleIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) {
|
if (
|
||||||
|
flags.finalizeExam &&
|
||||||
|
!flags.pendingEvaluation &&
|
||||||
|
pendingExercises.length === 0
|
||||||
|
) {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (evaluated.length !== 0) {
|
if (evaluated.length !== 0) {
|
||||||
setUserSolutions(
|
setUserSolutions(
|
||||||
userSolutions.map(solution => {
|
userSolutions.map((solution) => {
|
||||||
const evaluatedSolution = evaluated.find(e => e.exercise === solution.exercise);
|
const evaluatedSolution = evaluated.find(
|
||||||
|
(e) => e.exercise === solution.exercise
|
||||||
|
);
|
||||||
if (evaluatedSolution) {
|
if (evaluatedSolution) {
|
||||||
return { ...solution, ...evaluatedSolution };
|
return { ...solution, ...evaluatedSolution };
|
||||||
}
|
}
|
||||||
@@ -171,14 +216,23 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
await axios.get("/api/stats/update");
|
await axios.get("/api/stats/update");
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setFlags({ finalizeExam: false });
|
setFlags({ finalizeExam: false });
|
||||||
dispatch({ type: "UPDATE_EXAMS" })
|
dispatch({ type: "UPDATE_EXAMS" });
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions, flags]);
|
}, [
|
||||||
|
saveStats,
|
||||||
|
setFlags,
|
||||||
|
setModuleIndex,
|
||||||
|
evaluated,
|
||||||
|
pendingExercises,
|
||||||
|
setUserSolutions,
|
||||||
|
flags,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const aggregateScoresByModule = (
|
||||||
const aggregateScoresByModule = (isPractice?: boolean): {
|
isPractice?: boolean
|
||||||
|
): {
|
||||||
module: Module;
|
module: Module;
|
||||||
total: number;
|
total: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
@@ -214,29 +268,39 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
userSolutions.filter(x => isPractice ? x.isPractice : !x.isPractice).forEach((x) => {
|
userSolutions.forEach((x) => {
|
||||||
|
if ((isPractice && x.isPractice) || (!isPractice && !x.isPractice)) {
|
||||||
const examModule =
|
const examModule =
|
||||||
x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined);
|
x.module ||
|
||||||
|
(x.type === "writing"
|
||||||
|
? "writing"
|
||||||
|
: x.type === "speaking" || x.type === "interactiveSpeaking"
|
||||||
|
? "speaking"
|
||||||
|
: undefined);
|
||||||
|
|
||||||
scores[examModule!] = {
|
scores[examModule!] = {
|
||||||
total: scores[examModule!].total + x.score.total,
|
total: scores[examModule!].total + x.score.total,
|
||||||
correct: scores[examModule!].correct + x.score.correct,
|
correct: scores[examModule!].correct + x.score.correct,
|
||||||
missing: scores[examModule!].missing + x.score.missing,
|
missing: scores[examModule!].missing + x.score.missing,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.keys(scores)
|
return Object.keys(scores).reduce((acc, x) => {
|
||||||
.filter((x) => scores[x as Module].total > 0)
|
if (scores[x as Module].total > 0) {
|
||||||
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
|
acc.push({ module: x as Module, ...scores[x as Module] });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as any[]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModuleExamMap: Record<Module, React.ComponentType<ExamProps<Exam>>> = {
|
const ModuleExamMap: Record<Module, React.ComponentType<ExamProps<Exam>>> = {
|
||||||
"reading": Reading as React.ComponentType<ExamProps<Exam>>,
|
reading: Reading as React.ComponentType<ExamProps<Exam>>,
|
||||||
"listening": Listening as React.ComponentType<ExamProps<Exam>>,
|
listening: Listening as React.ComponentType<ExamProps<Exam>>,
|
||||||
"writing": Writing as React.ComponentType<ExamProps<Exam>>,
|
writing: Writing as React.ComponentType<ExamProps<Exam>>,
|
||||||
"speaking": Speaking as React.ComponentType<ExamProps<Exam>>,
|
speaking: Speaking as React.ComponentType<ExamProps<Exam>>,
|
||||||
"level": Level as React.ComponentType<ExamProps<Exam>>,
|
level: Level as React.ComponentType<ExamProps<Exam>>,
|
||||||
}
|
};
|
||||||
|
|
||||||
const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined;
|
const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined;
|
||||||
|
|
||||||
@@ -245,36 +309,71 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
setBgColor,
|
||||||
|
setHideSidebar,
|
||||||
|
setFocusMode,
|
||||||
|
setOnFocusLayerMouseEnter,
|
||||||
|
} = React.useContext(LayoutContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOnFocusLayerMouseEnter(() => () => setShowAbandonPopup(true));
|
||||||
|
}, [
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBgColor(bgColor);
|
||||||
|
setHideSidebar(hideSidebar);
|
||||||
|
setFocusMode(
|
||||||
|
selectedModules.length !== 0 &&
|
||||||
|
!showSolutions &&
|
||||||
|
moduleIndex < selectedModules.length
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
bgColor,
|
||||||
|
hideSidebar,
|
||||||
|
moduleIndex,
|
||||||
|
selectedModules.length,
|
||||||
|
setBgColor,
|
||||||
|
setFocusMode,
|
||||||
|
setHideSidebar,
|
||||||
|
showSolutions,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout
|
|
||||||
user={user}
|
|
||||||
bgColor={bgColor}
|
|
||||||
hideSidebar={hideSidebar}
|
|
||||||
className="justify-between"
|
|
||||||
focusMode={selectedModules.length !== 0 && !showSolutions && moduleIndex < selectedModules.length}
|
|
||||||
onFocusLayerMouseEnter={() => setShowAbandonPopup(true)}>
|
|
||||||
<>
|
<>
|
||||||
{/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/}
|
{/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/}
|
||||||
{selectedModules.length === 0 && <Selection
|
{selectedModules.length === 0 && (
|
||||||
|
<Selection
|
||||||
page={page}
|
page={page}
|
||||||
user={user!}
|
user={user!}
|
||||||
onStart={(modules: Module[], avoid: boolean, variant: Variant) => {
|
onStart={(
|
||||||
|
modules: Module[],
|
||||||
|
avoid: boolean,
|
||||||
|
variant: Variant
|
||||||
|
) => {
|
||||||
setModuleIndex(0);
|
setModuleIndex(0);
|
||||||
setAvoidRepeated(avoid);
|
setAvoidRepeated(avoid);
|
||||||
setSelectedModules(modules);
|
setSelectedModules(modules);
|
||||||
setVariant(variant);
|
setVariant(variant);
|
||||||
}}
|
}}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
{isFetchingExams && (
|
{isFetchingExams && (
|
||||||
<div className="flex flex-grow flex-col items-center justify-center animate-pulse">
|
<div className="flex flex-grow flex-col items-center justify-center animate-pulse">
|
||||||
<span className={`loading loading-infinity w-32 bg-ielts-${selectedModules[0]}`} />
|
<span
|
||||||
<span className={`font-bold text-2xl text-ielts-${selectedModules[0]}`}>Loading Exam ...</span>
|
className={`loading loading-infinity w-32 bg-ielts-${selectedModules[0]}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`font-bold text-2xl text-ielts-${selectedModules[0]}`}
|
||||||
|
>
|
||||||
|
Loading Exam ...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(moduleIndex === -1 && selectedModules.length !== 0) &&
|
{moduleIndex === -1 && selectedModules.length !== 0 && (
|
||||||
<Finish
|
<Finish
|
||||||
isLoading={flags.pendingEvaluation}
|
isLoading={flags.pendingEvaluation}
|
||||||
user={user!}
|
user={user!}
|
||||||
@@ -289,11 +388,19 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
onViewResults={(index?: number) => {
|
onViewResults={(index?: number) => {
|
||||||
if (exams[0].module === "level") {
|
if (exams[0].module === "level") {
|
||||||
const levelExam = exams[0] as LevelExam;
|
const levelExam = exams[0] as LevelExam;
|
||||||
const allExercises = levelExam.parts.flatMap((part) => part.exercises);
|
const allExercises = levelExam.parts.flatMap(
|
||||||
const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index]));
|
(part) => part.exercises
|
||||||
const orderedSolutions = userSolutions.slice().sort((a, b) => {
|
);
|
||||||
const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity;
|
const exerciseOrderMap = new Map(
|
||||||
const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity;
|
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;
|
return indexA - indexB;
|
||||||
});
|
});
|
||||||
setUserSolutions(orderedSolutions);
|
setUserSolutions(orderedSolutions);
|
||||||
@@ -316,12 +423,16 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
}}
|
}}
|
||||||
scores={aggregateScoresByModule()}
|
scores={aggregateScoresByModule()}
|
||||||
practiceScores={aggregateScoresByModule(true)}
|
practiceScores={aggregateScoresByModule(true)}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
{/* Exam is on going, display it and the abandon modal */}
|
{/* Exam is on going, display it and the abandon modal */}
|
||||||
{isExamLoaded && moduleIndex !== -1 && (
|
{isExamLoaded && moduleIndex !== -1 && (
|
||||||
<>
|
<>
|
||||||
{exam && CurrentExam && <CurrentExam exam={exam} showSolutions={showSolutions} />}
|
{exam && CurrentExam && (
|
||||||
{!showSolutions && <AbandonPopup
|
<CurrentExam exam={exam} showSolutions={showSolutions} />
|
||||||
|
)}
|
||||||
|
{!showSolutions && (
|
||||||
|
<AbandonPopup
|
||||||
isOpen={showAbandonPopup}
|
isOpen={showAbandonPopup}
|
||||||
abandonPopupTitle="Leave Exercise"
|
abandonPopupTitle="Leave Exercise"
|
||||||
abandonPopupDescription="Are you sure you want to leave the exercise? Your progress will be saved and this exam can be resumed on the Dashboard."
|
abandonPopupDescription="Are you sure you want to leave the exercise? Your progress will be saved and this exam can be resumed on the Dashboard."
|
||||||
@@ -329,11 +440,10 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
onAbandon={onAbandon}
|
onAbandon={onAbandon}
|
||||||
onCancel={() => setShowAbandonPopup(false)}
|
onCancel={() => setShowAbandonPopup(false)}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Layout>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* 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 useUsers from "@/hooks/useUsers";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { capitalize, sortBy } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import useInvites from "@/hooks/useInvites";
|
import useInvites from "@/hooks/useInvites";
|
||||||
import { BsArrowRepeat } from "react-icons/bs";
|
import { BsArrowRepeat } from "react-icons/bs";
|
||||||
import InviteCard from "@/components/Medium/InviteCard";
|
import InviteCard from "@/components/Medium/InviteCard";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import useDiscounts from "@/hooks/useDiscounts";
|
|
||||||
import PaymobPayment from "@/components/PaymobPayment";
|
import PaymobPayment from "@/components/PaymobPayment";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
@@ -22,44 +18,65 @@ import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
|||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User
|
user: User;
|
||||||
discounts: Discount[]
|
discounts: Discount[];
|
||||||
packages: Package[]
|
packages: Package[];
|
||||||
entities: EntityWithRoles[]
|
entities: EntityWithRoles[];
|
||||||
hasExpired?: boolean;
|
hasExpired?: boolean;
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PaymentDue({ user, discounts = [], entities = [], packages = [], hasExpired = false, reload }: Props) {
|
export default function PaymentDue({
|
||||||
|
user,
|
||||||
|
discounts = [],
|
||||||
|
entities = [],
|
||||||
|
packages = [],
|
||||||
|
hasExpired = false,
|
||||||
|
reload,
|
||||||
|
}: Props) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [entity, setEntity] = useState<EntityWithRoles>()
|
const [entity, setEntity] = useState<EntityWithRoles>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const { invites, isLoading: isInvitesLoading, reload: reloadInvites } = useInvites({ to: user?.id });
|
const {
|
||||||
|
invites,
|
||||||
|
isLoading: isInvitesLoading,
|
||||||
|
reload: reloadInvites,
|
||||||
|
} = useInvites({ to: user?.id });
|
||||||
|
|
||||||
const isIndividual = useMemo(() => {
|
const isIndividual = useMemo(() => {
|
||||||
if (isAdmin(user)) return false;
|
if (isAdmin(user)) return false;
|
||||||
if (user?.type !== "student") return false;
|
if (user?.type !== "student") return false;
|
||||||
|
|
||||||
return user.entities.length === 0
|
return user.entities.length === 0;
|
||||||
}, [user])
|
}, [user]);
|
||||||
|
|
||||||
const appliedDiscount = useMemo(() => {
|
const appliedDiscount = useMemo(() => {
|
||||||
const biggestDiscount = [...discounts].sort((a, b) => b.percentage - a.percentage).shift();
|
const biggestDiscount = [...discounts]
|
||||||
|
.sort((a, b) => b.percentage - a.percentage)
|
||||||
|
.shift();
|
||||||
|
|
||||||
if (!biggestDiscount || (biggestDiscount.validUntil && moment(biggestDiscount.validUntil).isBefore(moment())))
|
if (
|
||||||
|
!biggestDiscount ||
|
||||||
|
(biggestDiscount.validUntil &&
|
||||||
|
moment(biggestDiscount.validUntil).isBefore(moment()))
|
||||||
|
)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return biggestDiscount.percentage
|
return biggestDiscount.percentage;
|
||||||
}, [discounts])
|
}, [discounts]);
|
||||||
|
|
||||||
const entitiesThatCanBePaid = useAllowedEntities(user, entities, 'pay_entity')
|
const entitiesThatCanBePaid = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"pay_entity"
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (entitiesThatCanBePaid.length > 0) setEntity(entitiesThatCanBePaid[0])
|
if (entitiesThatCanBePaid.length > 0) setEntity(entitiesThatCanBePaid[0]);
|
||||||
}, [entitiesThatCanBePaid])
|
}, [entitiesThatCanBePaid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -67,26 +84,42 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
|
<div className="absolute left-0 top-0 z-[999] h-screen w-screen overflow-hidden bg-black/60">
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-8 text-white">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-8 text-white">
|
||||||
<span className={clsx("loading loading-infinity w-48 animate-pulse")} />
|
<span
|
||||||
<span className={clsx("text-2xl font-bold animate-pulse")}>Completing your payment...</span>
|
className={clsx("loading loading-infinity w-48 animate-pulse")}
|
||||||
<span>If you canceled your payment or it failed, please click the button below to restart</span>
|
/>
|
||||||
|
<span className={clsx("text-2xl font-bold animate-pulse")}>
|
||||||
|
Completing your payment...
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
If you canceled your payment or it failed, please click the button
|
||||||
|
below to restart
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsLoading(false)}
|
onClick={() => setIsLoading(false)}
|
||||||
className="border border-white rounded-full px-4 py-2 hover:bg-white/80 hover:text-black cursor-pointer transition ease-in-out duration-300">
|
className="border border-white rounded-full px-4 py-2 hover:bg-white/80 hover:text-black cursor-pointer transition ease-in-out duration-300"
|
||||||
|
>
|
||||||
Cancel Payment
|
Cancel Payment
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Layout user={user} navDisabled={hasExpired}>
|
<>
|
||||||
{invites.length > 0 && (
|
{invites.length > 0 && (
|
||||||
<section className="flex flex-col gap-1 md:gap-3">
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
onClick={reloadInvites}
|
onClick={reloadInvites}
|
||||||
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out"
|
||||||
<span className="text-mti-black text-lg font-bold">Invites</span>
|
>
|
||||||
<BsArrowRepeat className={clsx("text-xl", isInvitesLoading && "animate-spin")} />
|
<span className="text-mti-black text-lg font-bold">
|
||||||
|
Invites
|
||||||
|
</span>
|
||||||
|
<BsArrowRepeat
|
||||||
|
className={clsx(
|
||||||
|
"text-xl",
|
||||||
|
isInvitesLoading && "animate-spin"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
@@ -106,21 +139,40 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
|
<div className="flex w-full flex-col items-center justify-center gap-4 text-center">
|
||||||
{hasExpired && <span className="text-lg font-bold">You do not have time credits for your account type!</span>}
|
{hasExpired && (
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
You do not have time credits for your account type!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isIndividual && (
|
{isIndividual && (
|
||||||
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
|
<div className="scrollbar-hide flex w-full flex-col items-center gap-12 overflow-x-scroll">
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
To add to your use of EnCoach, please purchase one of the time packages available below:
|
To add to your use of EnCoach, please purchase one of the time
|
||||||
|
packages available below:
|
||||||
</span>
|
</span>
|
||||||
<div className="flex w-full flex-wrap justify-center gap-8">
|
<div className="flex w-full flex-wrap justify-center gap-8">
|
||||||
{packages.map((p) => (
|
{packages.map((p) => (
|
||||||
<div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-start gap-6 rounded-xl bg-white p-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mb-2 flex flex-col items-start">
|
<div className="mb-2 flex flex-col items-start">
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<img
|
||||||
|
src="/logo_title.png"
|
||||||
|
alt="EnCoach's Logo"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
<span className="text-xl font-semibold">
|
<span className="text-xl font-semibold">
|
||||||
EnCoach - {p.duration}{" "}
|
EnCoach - {p.duration}{" "}
|
||||||
{capitalize(
|
{capitalize(
|
||||||
p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit,
|
p.duration === 1
|
||||||
|
? p.duration_unit.slice(
|
||||||
|
0,
|
||||||
|
p.duration_unit.length - 1
|
||||||
|
)
|
||||||
|
: p.duration_unit
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +188,11 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
{p.price} {p.currency}
|
{p.price} {p.currency}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-2xl text-mti-red-light">
|
<span className="text-2xl text-mti-red-light">
|
||||||
{(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency}
|
{(
|
||||||
|
p.price -
|
||||||
|
p.price * (appliedDiscount / 100)
|
||||||
|
).toFixed(2)}{" "}
|
||||||
|
{p.currency}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -149,15 +205,24 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
currency={p.currency}
|
currency={p.currency}
|
||||||
duration={p.duration}
|
duration={p.duration}
|
||||||
duration_unit={p.duration_unit}
|
duration_unit={p.duration_unit}
|
||||||
price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)}
|
price={
|
||||||
|
+(
|
||||||
|
p.price -
|
||||||
|
p.price * (appliedDiscount / 100)
|
||||||
|
).toFixed(2)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start gap-1">
|
<div className="flex flex-col items-start gap-1">
|
||||||
<span>This includes:</span>
|
<span>This includes:</span>
|
||||||
<ul className="flex flex-col items-start text-sm">
|
<ul className="flex flex-col items-start text-sm">
|
||||||
<li>- Train your abilities for the IELTS exam</li>
|
<li>- Train your abilities for the IELTS exam</li>
|
||||||
<li>- Gain insights into your weaknesses and strengths</li>
|
<li>
|
||||||
<li>- Allow yourself to correctly prepare for the exam</li>
|
- Gain insights into your weaknesses and strengths
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
- Allow yourself to correctly prepare for the exam
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,26 +231,43 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isIndividual && entitiesThatCanBePaid.length > 0 &&
|
{!isIndividual &&
|
||||||
|
entitiesThatCanBePaid.length > 0 &&
|
||||||
entity?.payment && (
|
entity?.payment && (
|
||||||
<div className="flex flex-col items-center gap-8">
|
<div className="flex flex-col items-center gap-8">
|
||||||
<div className={clsx("flex flex-col items-center gap-4 w-full")}>
|
<div
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
className={clsx("flex flex-col items-center gap-4 w-full")}
|
||||||
|
>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Entity
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={{ value: entity?.id, label: entity?.label }}
|
defaultValue={{ value: entity?.id, label: entity?.label }}
|
||||||
options={entitiesThatCanBePaid.map((e) => ({ value: e.id, label: e.label, entity: e }))}
|
options={entitiesThatCanBePaid.map((e) => ({
|
||||||
onChange={(e) => e?.value ? setEntity(e?.entity) : null}
|
value: e.id,
|
||||||
|
label: e.label,
|
||||||
|
entity: e,
|
||||||
|
}))}
|
||||||
|
onChange={(e) => (e?.value ? setEntity(e?.entity) : null)}
|
||||||
className="!w-full max-w-[400px] self-center"
|
className="!w-full max-w-[400px] self-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
To add to your use of EnCoach and that of your students and teachers, please pay your designated package
|
To add to your use of EnCoach and that of your students and
|
||||||
below:
|
teachers, please pay your designated package below:
|
||||||
</span>
|
</span>
|
||||||
<div className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col items-start gap-6 rounded-xl bg-white p-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="mb-2 flex flex-col items-start">
|
<div className="mb-2 flex flex-col items-start">
|
||||||
<img src="/logo_title.png" alt="EnCoach's Logo" className="w-32" />
|
<img
|
||||||
|
src="/logo_title.png"
|
||||||
|
alt="EnCoach's Logo"
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
<span className="text-xl font-semibold">
|
<span className="text-xl font-semibold">
|
||||||
EnCoach - {12} Months
|
EnCoach - {12} Months
|
||||||
</span>
|
</span>
|
||||||
@@ -212,10 +294,14 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
<span>This includes:</span>
|
<span>This includes:</span>
|
||||||
<ul className="flex flex-col items-start text-sm">
|
<ul className="flex flex-col items-start text-sm">
|
||||||
<li>
|
<li>
|
||||||
- Allow a total of {entity.licenses} students and teachers to use EnCoach
|
- Allow a total of {entity.licenses} students and
|
||||||
|
teachers to use EnCoach
|
||||||
</li>
|
</li>
|
||||||
<li>- Train their abilities for the IELTS exam</li>
|
<li>- Train their abilities for the IELTS exam</li>
|
||||||
<li>- Gain insights into your students' weaknesses and strengths</li>
|
<li>
|
||||||
|
- Gain insights into your students' weaknesses and
|
||||||
|
strengths
|
||||||
|
</li>
|
||||||
<li>- Allow them to correctly prepare for the exam</li>
|
<li>- Allow them to correctly prepare for the exam</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,11 +311,12 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
{!isIndividual && entitiesThatCanBePaid.length === 0 && (
|
{!isIndividual && entitiesThatCanBePaid.length === 0 && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
You are not the person in charge of your time credits, please contact your administrator about this situation.
|
You are not the person in charge of your time credits, please
|
||||||
|
contact your administrator about this situation.
|
||||||
</span>
|
</span>
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
If you believe this to be a mistake, please contact the platform's administration, thank you for your
|
If you believe this to be a mistake, please contact the
|
||||||
patience.
|
platform's administration, thank you for your patience.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -237,26 +324,39 @@ export default function PaymentDue({ user, discounts = [], entities = [], packag
|
|||||||
entitiesThatCanBePaid.length > 0 &&
|
entitiesThatCanBePaid.length > 0 &&
|
||||||
!entity?.payment && (
|
!entity?.payment && (
|
||||||
<div className="flex flex-col items-center gap-8">
|
<div className="flex flex-col items-center gap-8">
|
||||||
<div className={clsx("flex flex-col items-center gap-4 w-full")}>
|
<div
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Entity</label>
|
className={clsx("flex flex-col items-center gap-4 w-full")}
|
||||||
|
>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Entity
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={{ value: entity?.id || "", label: entity?.label || "" }}
|
defaultValue={{
|
||||||
options={entitiesThatCanBePaid.map((e) => ({ value: e.id, label: e.label, entity: e }))}
|
value: entity?.id || "",
|
||||||
onChange={(e) => e?.value ? setEntity(e?.entity) : null}
|
label: entity?.label || "",
|
||||||
|
}}
|
||||||
|
options={entitiesThatCanBePaid.map((e) => ({
|
||||||
|
value: e.id,
|
||||||
|
label: e.label,
|
||||||
|
entity: e,
|
||||||
|
}))}
|
||||||
|
onChange={(e) => (e?.value ? setEntity(e?.entity) : null)}
|
||||||
className="!w-full max-w-[400px] self-center"
|
className="!w-full max-w-[400px] self-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users
|
An admin nor your agent have yet set the price intended to
|
||||||
you desire and your expected monthly duration.
|
your requirements in terms of the amount of users you desire
|
||||||
|
and your expected monthly duration.
|
||||||
</span>
|
</span>
|
||||||
<span className="max-w-lg">
|
<span className="max-w-lg">
|
||||||
Please try again later or contact your agent or an admin, thank you for your patience.
|
Please try again later or contact your agent or an admin,
|
||||||
|
thank you for your patience.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,47 @@ import "primereact/resources/themes/lara-light-indigo/theme.css";
|
|||||||
import "primereact/resources/primereact.min.css";
|
import "primereact/resources/primereact.min.css";
|
||||||
import "primeicons/primeicons.css";
|
import "primeicons/primeicons.css";
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import {useRouter} from "next/router";
|
import { Router, useRouter } from "next/router";
|
||||||
import {useEffect} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import usePreferencesStore from "@/stores/preferencesStore";
|
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) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { reset } = useExamStore();
|
const { reset } = useExamStore();
|
||||||
const setIsSidebarMinimized = usePreferencesStore((state) => state.setSidebarMinimized);
|
|
||||||
|
const setIsSidebarMinimized = usePreferencesStore(
|
||||||
|
(state) => state.setSidebarMinimized
|
||||||
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { entities } = useEntities(!pageProps?.user?.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.pathname !== "/exam" && router.pathname !== "/exercises") reset();
|
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]);
|
}, [router.pathname, reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,5 +59,13 @@ export default function App({Component, pageProps}: AppProps) {
|
|||||||
}
|
}
|
||||||
}, [setIsSidebarMinimized]);
|
}, [setIsSidebarMinimized]);
|
||||||
|
|
||||||
return <Component {...pageProps} />;
|
return (
|
||||||
|
<Layout user={pageProps.user} entities={entities} refreshPage={loading}>
|
||||||
|
{loading ? (
|
||||||
|
<UserProfileSkeleton />
|
||||||
|
) : (
|
||||||
|
<Component {...pageProps} entities={entities} />
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import client from "@/lib/mongodb";
|
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
|
import { getDetailedStatsByUser } from "../../../../utils/stats.be";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
@@ -14,11 +13,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {user} = req.query;
|
const { user, query } = req.query as { user: string, query?: string };
|
||||||
const snapshot = await db.collection("stats").aggregate([
|
|
||||||
{ $match: { user: user } },
|
|
||||||
{ $sort: { "date": 1 } }
|
|
||||||
]).toArray();
|
|
||||||
|
|
||||||
|
const snapshot = await getDetailedStatsByUser(user, query);
|
||||||
res.status(200).json(snapshot);
|
res.status(200).json(snapshot);
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import Modal from "@/components/Modal";
|
|
||||||
import useUsers from "@/hooks/useUsers";
|
|
||||||
import { Grading, Module } from "@/interfaces";
|
import { Grading, Module } from "@/interfaces";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import { Group, Stat, User } from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import { getExamById } from "@/utils/exams";
|
import { getExamById } from "@/utils/exams";
|
||||||
import { sortByModule } from "@/utils/moduleUtils";
|
import { sortByModule } from "@/utils/moduleUtils";
|
||||||
import { calculateBandScore, getGradingLabel } from "@/utils/score";
|
import { calculateBandScore, getGradingLabel } from "@/utils/score";
|
||||||
import { convertToUserSolutions } from "@/utils/stats";
|
|
||||||
import { getUserName } from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -23,13 +20,11 @@ import { withIronSessionSsr } from "iron-session/next";
|
|||||||
import { checkAccess, doesEntityAllow } from "@/utils/permissions";
|
import { checkAccess, doesEntityAllow } from "@/utils/permissions";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getAssignment } from "@/utils/assignments.be";
|
import { getAssignment } from "@/utils/assignments.be";
|
||||||
import { getEntitiesUsers, getEntityUsers, getUsers } from "@/utils/users.be";
|
import { getEntityUsers, getUsers } from "@/utils/users.be";
|
||||||
import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be";
|
import { getEntityWithRoles } from "@/utils/entities.be";
|
||||||
import { getGroups, getGroupsByEntities, getGroupsByEntity } from "@/utils/groups.be";
|
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
@@ -353,7 +348,7 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-full flex items-center justify-between">
|
<div className="w-full flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -466,7 +461,7 @@ export default function AssignmentView({ user, users, entity, assignment, gradin
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
@@ -212,7 +211,7 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link href="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||||
@@ -589,7 +588,7 @@ export default function AssignmentsPage({ assignment, user, users, entities, gro
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
@@ -170,7 +169,7 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link href="/assignments" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||||
@@ -528,7 +527,7 @@ export default function AssignmentsPage({ user, users, groups, entities }: Props
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import AssignmentCard from "@/components/AssignmentCard";
|
import AssignmentCard from "@/components/AssignmentCard";
|
||||||
import AssignmentView from "@/components/AssignmentView";
|
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import { CorporateUser, Group, User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { getUserCompanyName } from "@/resources/user";
|
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
@@ -21,15 +18,13 @@ import {
|
|||||||
} from "@/utils/assignments";
|
} from "@/utils/assignments";
|
||||||
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
import { getAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
|
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { groupBy } from "lodash";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { BsChevronLeft, BsPlus } from "react-icons/bs";
|
import { BsChevronLeft, BsPlus } from "react-icons/bs";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
@@ -98,7 +93,7 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/dashboard" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
<Link href="/dashboard" className="text-mti-purple hover:text-mti-purple-dark transition ease-in-out duration-300 text-xl">
|
||||||
@@ -223,7 +218,7 @@ export default function AssignmentsPage({ assignments, entities, user, users }:
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
@@ -183,7 +182,7 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -339,7 +338,7 @@ export default function Home({ user, group, users, entity }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import {useListSearch} from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import {Entity, EntityWithRoles} from "@/interfaces/entity";
|
import {EntityWithRoles} from "@/interfaces/entity";
|
||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||||
import {filterBy, mapBy, redirect, serialize} from "@/utils";
|
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 {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import {getUserName, isAdmin} from "@/utils/users";
|
import {getUserName, isAdmin} from "@/utils/users";
|
||||||
import {getEntitiesUsers, getLinkedUsers} from "@/utils/users.be";
|
import {getEntitiesUsers} from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
@@ -102,7 +101,7 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex gap-3 justify-between">
|
<div className="flex gap-3 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -217,7 +216,7 @@ export default function Home({user, users, entities}: Props) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Head from "next/head";
|
|||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import { GroupWithUsers, User } from "@/interfaces/user";
|
import { GroupWithUsers, User } from "@/interfaces/user";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { getUserName, isAdmin } from "@/utils/users";
|
import { getUserName, isAdmin } from "@/utils/users";
|
||||||
@@ -11,13 +10,13 @@ import { convertToUsers, getGroupsForEntities } from "@/utils/groups.be";
|
|||||||
import { getSpecificUsers } from "@/utils/users.be";
|
import { getSpecificUsers } from "@/utils/users.be";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { uniq } from "lodash";
|
import { uniq } from "lodash";
|
||||||
import { BsFillMortarboardFill, BsPlus } from "react-icons/bs";
|
import { BsPlus } from "react-icons/bs";
|
||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { findAllowedEntities } from "@/utils/permissions";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { FaPersonChalkboard } from "react-icons/fa6";
|
import { FaPersonChalkboard } from "react-icons/fa6";
|
||||||
@@ -122,7 +121,7 @@ export default function Home({ user, groups, entities }: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user} className="!gap-4">
|
<>
|
||||||
<section className="flex flex-col gap-4 w-full h-full">
|
<section className="flex flex-col gap-4 w-full h-full">
|
||||||
<Modal isOpen={showImport} onClose={() => setShowImport(false)} maxWidth="max-w-[85%]">
|
<Modal isOpen={showImport} onClose={() => setShowImport(false)} maxWidth="max-w-[85%]">
|
||||||
<StudentClassroomTransfer user={user} entities={entities} onFinish={() => setShowImport(false)} />
|
<StudentClassroomTransfer user={user} entities={entities} onFinish={() => setShowImport(false)} />
|
||||||
@@ -153,7 +152,7 @@ export default function Home({ user, groups, entities }: Props) {
|
|||||||
firstCard={entitiesAllowCreate.length === 0 ? undefined : firstCard}
|
firstCard={entitiesAllowCreate.length === 0 ? undefined : firstCard}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Stat, Type, User } from "@/interfaces/user";
|
||||||
import { Group, Stat, Type, User } from "@/interfaces/user";
|
|
||||||
import { sessionOptions } from "@/lib/session";
|
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 { requestUser } from "@/utils/api";
|
||||||
import { countEntitiesAssignments, getAssignments } from "@/utils/assignments.be";
|
import {
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
countEntitiesAssignments,
|
||||||
import { countGroups, getGroups } from "@/utils/groups.be";
|
} from "@/utils/assignments.be";
|
||||||
|
import { getEntities } from "@/utils/entities.be";
|
||||||
|
import { countGroups } from "@/utils/groups.be";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
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 { withIronSessionSsr } from "iron-session/next";
|
||||||
import { uniqBy } from "lodash";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
|
||||||
import {
|
import {
|
||||||
BsBank,
|
BsBank,
|
||||||
BsClipboard2Data,
|
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsPencilSquare,
|
BsPencilSquare,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
@@ -36,39 +35,78 @@ import { ToastContainer } from "react-toastify";
|
|||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: User[];
|
students: User[];
|
||||||
latestStudents: User[]
|
latestStudents: User[];
|
||||||
latestTeachers: User[]
|
latestTeachers: User[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
usersCount: { [key in Type]: number }
|
usersCount: { [key in Type]: number };
|
||||||
assignmentsCount: number;
|
assignmentsCount: number;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
groupsCount: number;
|
groupsCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user || !user.isVerified) return redirect("/login")
|
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 students = await getUsers(
|
||||||
const usersCount = {
|
{ type: "student" },
|
||||||
student: await countUsers({ type: "student" }),
|
10,
|
||||||
teacher: await countUsers({ type: "teacher" }),
|
{
|
||||||
corporate: await countUsers({ type: "corporate" }),
|
averageLevel: -1,
|
||||||
mastercorporate: await countUsers({ type: "mastercorporate" }),
|
},
|
||||||
}
|
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
const latestStudents = await getUsers({ type: 'student' }, 10, { registrationDate: -1 })
|
const usersCount = await countUsersByTypes([
|
||||||
const latestTeachers = await getUsers({ type: 'teacher' }, 10, { registrationDate: -1 })
|
"student",
|
||||||
|
"teacher",
|
||||||
|
"corporate",
|
||||||
|
"mastercorporate",
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 entities = await getEntities(undefined, { _id: 0, id: 1, label: 1 });
|
||||||
|
|
||||||
|
const assignmentsCount = await countEntitiesAssignments(
|
||||||
|
mapBy(entities, "id"),
|
||||||
|
{ archived: { $ne: true } }
|
||||||
|
);
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles();
|
|
||||||
const assignmentsCount = await countEntitiesAssignments(mapBy(entities, 'id'), { archived: { $ne: true } });
|
|
||||||
const groupsCount = await countGroups();
|
const groupsCount = await countGroups();
|
||||||
|
|
||||||
const stats = await getStatsByUsers(mapBy(students, 'id'));
|
const stats = await getStatsByUsers(mapBy(students, "id"));
|
||||||
|
|
||||||
return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, stats, groupsCount }) };
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
students,
|
||||||
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
usersCount,
|
||||||
|
entities,
|
||||||
|
assignmentsCount,
|
||||||
|
stats,
|
||||||
|
groupsCount,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({
|
export default function Dashboard({
|
||||||
@@ -80,7 +118,7 @@ export default function Dashboard({
|
|||||||
entities,
|
entities,
|
||||||
assignmentsCount,
|
assignmentsCount,
|
||||||
stats,
|
stats,
|
||||||
groupsCount
|
groupsCount,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -96,7 +134,7 @@ export default function Dashboard({
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/users?type=student")}
|
onClick={() => router.push("/users?type=student")}
|
||||||
@@ -133,19 +171,22 @@ export default function Dashboard({
|
|||||||
value={groupsCount}
|
value={groupsCount}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPeopleFill}
|
<IconCard
|
||||||
|
Icon={BsPeopleFill}
|
||||||
onClick={() => router.push("/entities")}
|
onClick={() => router.push("/entities")}
|
||||||
label="Entities"
|
label="Entities"
|
||||||
value={entities.length}
|
value={entities.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/statistical")}
|
onClick={() => router.push("/statistical")}
|
||||||
label="Entity Statistics"
|
label="Entity Statistics"
|
||||||
value={entities.length}
|
value={entities.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/users/performance")}
|
onClick={() => router.push("/users/performance")}
|
||||||
label="Student Performance"
|
label="Student Performance"
|
||||||
value={usersCount.student}
|
value={usersCount.student}
|
||||||
@@ -161,31 +202,19 @@ export default function Dashboard({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList users={latestStudents} title="Latest Students" />
|
||||||
|
<UserDisplayList users={latestTeachers} title="Latest Teachers" />
|
||||||
|
<UserDisplayList users={students} title="Highest level students" />
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={latestStudents}
|
users={students.sort(
|
||||||
title="Latest Students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={latestTeachers}
|
|
||||||
title="Latest Teachers"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
|
||||||
title="Highest level students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={
|
|
||||||
students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Stat, StudentUser, Type, User } from "@/interfaces/user";
|
import { Stat, StudentUser, Type, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
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 { requestUser } from "@/utils/api";
|
||||||
import { countEntitiesAssignments } from "@/utils/assignments.be";
|
import { countEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { countGroupsByEntities } from "@/utils/groups.be";
|
import { countGroupsByEntities } from "@/utils/groups.be";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import {
|
||||||
import { calculateAverageLevel } from "@/utils/score";
|
checkAccess,
|
||||||
|
groupAllowedEntitiesByPermissions,
|
||||||
|
} from "@/utils/permissions";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
import {
|
||||||
import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be";
|
countAllowedUsers,
|
||||||
|
getUsers,
|
||||||
|
} from "@/utils/users.be";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { uniqBy } from "lodash";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
BsClipboard2Data,
|
|
||||||
BsClock,
|
BsClock,
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsPencilSquare,
|
BsPencilSquare,
|
||||||
@@ -37,10 +38,10 @@ import { isAdmin } from "@/utils/users";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: StudentUser[]
|
students: StudentUser[];
|
||||||
latestStudents: User[]
|
latestStudents: User[];
|
||||||
latestTeachers: User[]
|
latestTeachers: User[];
|
||||||
userCounts: { [key in Type]: number }
|
userCounts: { [key in Type]: number };
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignmentsCount: number;
|
assignmentsCount: number;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
@@ -48,38 +49,113 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user || !user.isVerified) return redirect("/login")
|
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 entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
|
const entities = await getEntitiesWithRoles(
|
||||||
|
isAdmin(user) ? undefined : entityIDS
|
||||||
|
);
|
||||||
|
|
||||||
const allowedStudentEntities = findAllowedEntities(user, entities, "view_students")
|
const {
|
||||||
const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers")
|
["view_students"]: allowedStudentEntities,
|
||||||
|
["view_teachers"]: allowedTeacherEntities,
|
||||||
|
} = groupAllowedEntitiesByPermissions(user, entities, [
|
||||||
|
"view_students",
|
||||||
|
"view_teachers",
|
||||||
|
]);
|
||||||
|
|
||||||
const students =
|
const allowedStudentEntitiesIDS = mapBy(allowedStudentEntities, "id");
|
||||||
await getUsers({ type: 'student', "entities.id": { $in: mapBy(allowedStudentEntities, 'id') } }, 10, { averageLevel: -1 });
|
const entitiesIDS = mapBy(entities, "id") || [];
|
||||||
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 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);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({ user, students, latestStudents, latestTeachers, userCounts, entities, assignmentsCount, stats = [], groupsCount }: Props) {
|
export default function Dashboard({
|
||||||
const totalCount = useMemo(() =>
|
user,
|
||||||
userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts])
|
students,
|
||||||
const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities])
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
userCounts,
|
||||||
|
entities,
|
||||||
|
assignmentsCount,
|
||||||
|
stats = [],
|
||||||
|
groupsCount,
|
||||||
|
}: Props) {
|
||||||
|
const totalCount = useMemo(
|
||||||
|
() =>
|
||||||
|
userCounts.corporate +
|
||||||
|
userCounts.mastercorporate +
|
||||||
|
userCounts.student +
|
||||||
|
userCounts.teacher,
|
||||||
|
[userCounts]
|
||||||
|
);
|
||||||
|
|
||||||
const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics')
|
const totalLicenses = useMemo(
|
||||||
const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance')
|
() =>
|
||||||
|
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();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -95,7 +171,7 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
@@ -124,14 +200,16 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
value={groupsCount}
|
value={groupsCount}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPeopleFill}
|
<IconCard
|
||||||
|
Icon={BsPeopleFill}
|
||||||
onClick={() => router.push("/entities")}
|
onClick={() => router.push("/entities")}
|
||||||
label="Entities"
|
label="Entities"
|
||||||
value={`${entities.length} - ${totalCount}/${totalLicenses}`}
|
value={`${entities.length} - ${totalCount}/${totalLicenses}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
{allowedEntityStatistics.length > 0 && (
|
{allowedEntityStatistics.length > 0 && (
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/statistical")}
|
onClick={() => router.push("/statistical")}
|
||||||
label="Entity Statistics"
|
label="Entity Statistics"
|
||||||
value={allowedEntityStatistics.length}
|
value={allowedEntityStatistics.length}
|
||||||
@@ -139,7 +217,8 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allowedStudentPerformance.length > 0 && (
|
{allowedStudentPerformance.length > 0 && (
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/users/performance")}
|
onClick={() => router.push("/users/performance")}
|
||||||
label="Student Performance"
|
label="Student Performance"
|
||||||
value={userCounts.student}
|
value={userCounts.student}
|
||||||
@@ -149,7 +228,11 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
value={
|
||||||
|
user.subscriptionExpirationDate
|
||||||
|
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
||||||
|
: "Unlimited"
|
||||||
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
@@ -164,31 +247,19 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList users={latestStudents} title="Latest Students" />
|
||||||
|
<UserDisplayList users={latestTeachers} title="Latest Teachers" />
|
||||||
|
<UserDisplayList users={students} title="Highest level students" />
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={latestStudents}
|
users={students.sort(
|
||||||
title="Latest Students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={latestTeachers}
|
|
||||||
title="Latest Teachers"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={students}
|
|
||||||
title="Highest level students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={
|
|
||||||
students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Stat, Type, User } from "@/interfaces/user";
|
||||||
import { Group, Stat, StudentUser, Type, User } from "@/interfaces/user";
|
|
||||||
import { sessionOptions } from "@/lib/session";
|
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 { requestUser } from "@/utils/api";
|
||||||
import { countEntitiesAssignments, getAssignments } from "@/utils/assignments.be";
|
import {
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
countEntitiesAssignments,
|
||||||
import { countGroups, getGroups } from "@/utils/groups.be";
|
} from "@/utils/assignments.be";
|
||||||
|
import { getEntities } from "@/utils/entities.be";
|
||||||
|
import { countGroups } from "@/utils/groups.be";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
import {
|
||||||
import { countUsers, getUser, getUsers } from "@/utils/users.be";
|
countUsersByTypes,
|
||||||
|
getUsers,
|
||||||
|
} from "@/utils/users.be";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { uniqBy } from "lodash";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
|
||||||
import {
|
import {
|
||||||
BsBank,
|
BsBank,
|
||||||
BsClipboard2Data,
|
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsPencilSquare,
|
BsPencilSquare,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
@@ -36,37 +34,73 @@ import { ToastContainer } from "react-toastify";
|
|||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: User[];
|
students: User[];
|
||||||
latestStudents: User[]
|
latestStudents: User[];
|
||||||
latestTeachers: User[]
|
latestTeachers: User[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
usersCount: { [key in Type]: number }
|
usersCount: { [key in Type]: number };
|
||||||
assignmentsCount: number;
|
assignmentsCount: number;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
groupsCount: number;
|
groupsCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user || !user.isVerified) return redirect("/login")
|
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 students = await getUsers(
|
||||||
const usersCount = {
|
{ type: "student" },
|
||||||
student: await countUsers({ type: "student" }),
|
10,
|
||||||
teacher: await countUsers({ type: "teacher" }),
|
{
|
||||||
corporate: await countUsers({ type: "corporate" }),
|
averageLevel: -1,
|
||||||
mastercorporate: await countUsers({ type: "mastercorporate" }),
|
},
|
||||||
}
|
{ id: 1, name: 1, email: 1, profilePicture: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
const latestStudents = await getUsers({ type: 'student' }, 10, { registrationDate: -1 })
|
const usersCount = await countUsersByTypes([
|
||||||
const latestTeachers = await getUsers({ type: 'teacher' }, 10, { registrationDate: -1 })
|
"student",
|
||||||
|
"teacher",
|
||||||
|
"corporate",
|
||||||
|
"mastercorporate",
|
||||||
|
]);
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles();
|
const latestStudents = await getUsers(
|
||||||
const assignmentsCount = await countEntitiesAssignments(mapBy(entities, 'id'), { archived: { $ne: true } });
|
{ 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 entities = await getEntities(undefined, { _id: 0, id: 1, label: 1 });
|
||||||
|
const assignmentsCount = await countEntitiesAssignments(
|
||||||
|
mapBy(entities, "id"),
|
||||||
|
{ archived: { $ne: true } }
|
||||||
|
);
|
||||||
const groupsCount = await countGroups();
|
const groupsCount = await countGroups();
|
||||||
|
|
||||||
return { props: serialize({ user, students, latestStudents, latestTeachers, usersCount, entities, assignmentsCount, groupsCount }) };
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
students,
|
||||||
|
latestStudents,
|
||||||
|
latestTeachers,
|
||||||
|
usersCount,
|
||||||
|
entities,
|
||||||
|
assignmentsCount,
|
||||||
|
groupsCount,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({
|
export default function Dashboard({
|
||||||
@@ -78,7 +112,7 @@ export default function Dashboard({
|
|||||||
entities,
|
entities,
|
||||||
assignmentsCount,
|
assignmentsCount,
|
||||||
stats = [],
|
stats = [],
|
||||||
groupsCount
|
groupsCount,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -94,7 +128,7 @@ export default function Dashboard({
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/users?type=student")}
|
onClick={() => router.push("/users?type=student")}
|
||||||
@@ -131,19 +165,22 @@ export default function Dashboard({
|
|||||||
value={groupsCount}
|
value={groupsCount}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPeopleFill}
|
<IconCard
|
||||||
|
Icon={BsPeopleFill}
|
||||||
onClick={() => router.push("/entities")}
|
onClick={() => router.push("/entities")}
|
||||||
label="Entities"
|
label="Entities"
|
||||||
value={entities.length}
|
value={entities.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/statistical")}
|
onClick={() => router.push("/statistical")}
|
||||||
label="Entity Statistics"
|
label="Entity Statistics"
|
||||||
value={entities.length}
|
value={entities.length}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/users/performance")}
|
onClick={() => router.push("/users/performance")}
|
||||||
label="Student Performance"
|
label="Student Performance"
|
||||||
value={usersCount.student}
|
value={usersCount.student}
|
||||||
@@ -159,31 +196,19 @@ export default function Dashboard({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList users={latestStudents} title="Latest Students" />
|
||||||
|
<UserDisplayList users={latestTeachers} title="Latest Teachers" />
|
||||||
|
<UserDisplayList users={students} title="Highest level students" />
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={latestStudents}
|
users={students.sort(
|
||||||
title="Latest Students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={latestTeachers}
|
|
||||||
title="Latest Teachers"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={students}
|
|
||||||
title="Highest level students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={
|
|
||||||
students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b.id))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a.id))).length
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,31 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { Module } from "@/interfaces";
|
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Stat, StudentUser, Type, User } from "@/interfaces/user";
|
||||||
import { Group, Stat, StudentUser, Type, User } from "@/interfaces/user";
|
|
||||||
import { sessionOptions } from "@/lib/session";
|
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 { requestUser } from "@/utils/api";
|
||||||
import { countEntitiesAssignments, getEntitiesAssignments } from "@/utils/assignments.be";
|
import { countEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { countGroupsByEntities, getGroupsByEntities } from "@/utils/groups.be";
|
import { countGroupsByEntities } from "@/utils/groups.be";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import {
|
||||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
checkAccess,
|
||||||
|
groupAllowedEntitiesByPermissions,
|
||||||
|
} from "@/utils/permissions";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
import { countAllowedUsers, getUsers } from "@/utils/users.be";
|
||||||
import { countAllowedUsers, filterAllowedUsers, getUsers } from "@/utils/users.be";
|
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { uniqBy } from "lodash";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
BsBank,
|
BsBank,
|
||||||
BsClipboard2Data,
|
|
||||||
BsClock,
|
BsClock,
|
||||||
BsEnvelopePaper,
|
BsEnvelopePaper,
|
||||||
BsPaperclip,
|
|
||||||
BsPencilSquare,
|
BsPencilSquare,
|
||||||
BsPeople,
|
BsPeople,
|
||||||
BsPeopleFill,
|
BsPeopleFill,
|
||||||
@@ -44,10 +37,10 @@ import { isAdmin } from "@/utils/users";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
students: StudentUser[]
|
students: StudentUser[];
|
||||||
latestStudents: User[]
|
latestStudents: User[];
|
||||||
latestTeachers: User[]
|
latestTeachers: User[];
|
||||||
userCounts: { [key in Type]: number }
|
userCounts: { [key in Type]: number };
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignmentsCount: number;
|
assignmentsCount: number;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
@@ -55,42 +48,115 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user || !user.isVerified) return redirect("/login")
|
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 entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
|
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 allowedStudentEntitiesIDS = mapBy(allowedStudentEntities, "id");
|
||||||
const allowedTeacherEntities = findAllowedEntities(user, entities, "view_teachers")
|
|
||||||
|
|
||||||
const students =
|
const entitiesIDS = mapBy(entities, "id") || [];
|
||||||
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 userCounts = await countAllowedUsers(user, entities)
|
const students = await getUsers(
|
||||||
const assignmentsCount = await countEntitiesAssignments(mapBy(entities, "id"), { archived: { $ne: true } });
|
{ type: "student", "entities.id": { $in: allowedStudentEntitiesIDS } },
|
||||||
const groupsCount = await countGroupsByEntities(mapBy(entities, "id"));
|
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);
|
}, 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(() =>
|
const totalLicenses = useMemo(
|
||||||
userCounts.corporate + userCounts.mastercorporate + userCounts.student + userCounts.teacher, [userCounts])
|
() =>
|
||||||
|
entities.reduce(
|
||||||
const totalLicenses = useMemo(() => entities.reduce((acc, curr) => acc + parseInt(curr.licenses.toString()), 0), [entities])
|
(acc, curr) => acc + parseInt(curr.licenses.toString()),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
[entities]
|
||||||
|
);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics')
|
const allowedEntityStatistics = useAllowedEntities(
|
||||||
const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance')
|
user,
|
||||||
|
entities,
|
||||||
|
"view_entity_statistics"
|
||||||
|
);
|
||||||
|
const allowedStudentPerformance = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"view_student_performance"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -104,7 +170,7 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
<section className="grid grid-cols-5 place-items-center -md:grid-cols-2 gap-4 text-center">
|
||||||
<IconCard
|
<IconCard
|
||||||
onClick={() => router.push("/users?type=student")}
|
onClick={() => router.push("/users?type=student")}
|
||||||
@@ -134,14 +200,16 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
value={groupsCount}
|
value={groupsCount}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard Icon={BsPeopleFill}
|
<IconCard
|
||||||
|
Icon={BsPeopleFill}
|
||||||
onClick={() => router.push("/entities")}
|
onClick={() => router.push("/entities")}
|
||||||
label="Entities"
|
label="Entities"
|
||||||
value={`${entities.length} - ${totalCount}/${totalLicenses}`}
|
value={`${entities.length} - ${totalCount}/${totalLicenses}`}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
{allowedStudentPerformance.length > 0 && (
|
{allowedStudentPerformance.length > 0 && (
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/users/performance")}
|
onClick={() => router.push("/users/performance")}
|
||||||
label="Student Performance"
|
label="Student Performance"
|
||||||
value={userCounts.student}
|
value={userCounts.student}
|
||||||
@@ -149,7 +217,8 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allowedEntityStatistics.length > 0 && (
|
{allowedEntityStatistics.length > 0 && (
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/statistical")}
|
onClick={() => router.push("/statistical")}
|
||||||
label="Entity Statistics"
|
label="Entity Statistics"
|
||||||
value={allowedEntityStatistics.length}
|
value={allowedEntityStatistics.length}
|
||||||
@@ -161,43 +230,37 @@ export default function Dashboard({ user, students, latestStudents, latestTeache
|
|||||||
onClick={() => router.push("/assignments")}
|
onClick={() => router.push("/assignments")}
|
||||||
label="Assignments"
|
label="Assignments"
|
||||||
value={assignmentsCount}
|
value={assignmentsCount}
|
||||||
className={clsx(allowedEntityStatistics.length === 0 && "col-span-2")}
|
className={clsx(
|
||||||
|
allowedEntityStatistics.length === 0 && "col-span-2"
|
||||||
|
)}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<IconCard
|
<IconCard
|
||||||
Icon={BsClock}
|
Icon={BsClock}
|
||||||
label="Expiration Date"
|
label="Expiration Date"
|
||||||
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"}
|
value={
|
||||||
|
user.subscriptionExpirationDate
|
||||||
|
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
|
||||||
|
: "Unlimited"
|
||||||
|
}
|
||||||
color="rose"
|
color="rose"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
|
<UserDisplayList users={latestStudents} title="Latest Students" />
|
||||||
|
<UserDisplayList users={latestTeachers} title="Latest Teachers" />
|
||||||
|
<UserDisplayList users={students} title="Highest level students" />
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={latestStudents}
|
users={students.sort(
|
||||||
title="Latest Students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={latestTeachers}
|
|
||||||
title="Latest Teachers"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={students}
|
|
||||||
title="Highest level students"
|
|
||||||
/>
|
|
||||||
<UserDisplayList
|
|
||||||
users={
|
|
||||||
students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import InviteWithUserCard from "@/components/Medium/InviteWithUserCard";
|
import InviteWithUserCard from "@/components/Medium/InviteWithUserCard";
|
||||||
@@ -10,39 +9,48 @@ import { Grading } from "@/interfaces";
|
|||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import { Exam } from "@/interfaces/exam";
|
||||||
import { InviteWithEntity } from "@/interfaces/invite";
|
import { InviteWithEntity } from "@/interfaces/invite";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment, AssignmentWithHasResults } from "@/interfaces/results";
|
||||||
import { Stat, User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { activeAssignmentFilter } from "@/utils/assignments";
|
import { getAssignmentsForStudent } from "@/utils/assignments.be";
|
||||||
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
import { getEntities } from "@/utils/entities.be";
|
||||||
import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be";
|
|
||||||
import { getExamsByIds } from "@/utils/exams.be";
|
import { getExamsByIds } from "@/utils/exams.be";
|
||||||
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
||||||
import { convertInvitersToEntity, getInvitesByInvitee } from "@/utils/invites.be";
|
import {
|
||||||
import { countExamModules, countFullExams, MODULE_ARRAY, sortByModule, sortByModuleName } from "@/utils/moduleUtils";
|
convertInvitersToEntity,
|
||||||
|
getInvitesByInvitee,
|
||||||
|
} from "@/utils/invites.be";
|
||||||
|
import { MODULE_ARRAY, sortByModule } from "@/utils/moduleUtils";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { getGradingLabel } from "@/utils/score";
|
import { getGradingLabel } from "@/utils/score";
|
||||||
import { getSessionsByUser } from "@/utils/sessions.be";
|
import { getSessionsByUser } from "@/utils/sessions.be";
|
||||||
import { averageScore } from "@/utils/stats";
|
import { getDetailedStatsByUser } from "@/utils/stats.be";
|
||||||
import { getStatsByUser } from "@/utils/stats.be";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { capitalize, uniqBy } from "lodash";
|
import { capitalize, uniqBy } from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import {
|
||||||
import { BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar } from "react-icons/bs";
|
BsBook,
|
||||||
|
BsClipboard,
|
||||||
|
BsFileEarmarkText,
|
||||||
|
BsHeadphones,
|
||||||
|
BsMegaphone,
|
||||||
|
BsPen,
|
||||||
|
BsPencil,
|
||||||
|
BsStar,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignments: Assignment[];
|
assignments: AssignmentWithHasResults[];
|
||||||
stats: Stat[];
|
stats: { fullExams: number; uniqueModules: number; averageScore: number };
|
||||||
exams: Exam[];
|
exams: Exam[];
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
invites: InviteWithEntity[];
|
invites: InviteWithEntity[];
|
||||||
@@ -50,60 +58,93 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user || !user.isVerified) return redirect("/login")
|
if (!user || !user.isVerified) return redirect("/login");
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "student"]))
|
if (!checkAccess(user, ["admin", "developer", "student"]))
|
||||||
return redirect("/")
|
return redirect("/");
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
const entities = await getEntities(entityIDS, { _id: 0, label: 1 });
|
||||||
const assignments = await getAssignmentsByAssignee(user.id, { archived: { $ne: true } });
|
const currentDate = moment().toISOString();
|
||||||
const stats = await getStatsByUser(user.id);
|
const assignments = await getAssignmentsForStudent(user.id, currentDate);
|
||||||
const sessions = await getSessionsByUser(user.id, 10);
|
const stats = await getDetailedStatsByUser(user.id, "stats");
|
||||||
|
|
||||||
|
const assignmentsIDs = mapBy(assignments, "id");
|
||||||
|
|
||||||
|
const sessions = await getSessionsByUser(user.id, 10, {
|
||||||
|
["assignment.id"]: { $in: assignmentsIDs },
|
||||||
|
});
|
||||||
const invites = await getInvitesByInvitee(user.id);
|
const invites = await getInvitesByInvitee(user.id);
|
||||||
const grading = await getGradingSystemByEntity(entityIDS[0] || "");
|
const grading = await getGradingSystemByEntity(entityIDS[0] || "", {
|
||||||
|
_id: 0,
|
||||||
const formattedInvites = await Promise.all(invites.map(convertInvitersToEntity));
|
steps: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedInvites = await Promise.all(
|
||||||
|
invites.map(convertInvitersToEntity)
|
||||||
|
);
|
||||||
const examIDs = uniqBy(
|
const examIDs = uniqBy(
|
||||||
assignments.flatMap((a) =>
|
assignments.flatMap((a) =>
|
||||||
a.exams.filter((e) => e.assignee === user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })),
|
a.exams.map((e: { module: string; id: string }) => ({
|
||||||
|
module: e.module,
|
||||||
|
id: e.id,
|
||||||
|
key: `${e.module}_${e.id}`,
|
||||||
|
}))
|
||||||
),
|
),
|
||||||
"key",
|
"key"
|
||||||
);
|
);
|
||||||
const exams = await getExamsByIds(examIDs);
|
const exams = examIDs.length > 0 ? await getExamsByIds(examIDs) : [];
|
||||||
|
|
||||||
return { props: serialize({ user, entities, assignments, stats, exams, sessions, invites: formattedInvites, grading }) };
|
return {
|
||||||
|
props: serialize({
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
assignments,
|
||||||
|
stats,
|
||||||
|
exams,
|
||||||
|
sessions,
|
||||||
|
invites: formattedInvites,
|
||||||
|
grading,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({ user, entities, assignments, stats, invites, grading, sessions, exams }: Props) {
|
export default function Dashboard({
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
assignments,
|
||||||
|
stats,
|
||||||
|
invites,
|
||||||
|
grading,
|
||||||
|
sessions,
|
||||||
|
exams,
|
||||||
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const dispatch = useExamStore((state) => state.dispatch);
|
const dispatch = useExamStore((state) => state.dispatch);
|
||||||
|
|
||||||
const startAssignment = (assignment: Assignment) => {
|
const startAssignment = (assignment: Assignment) => {
|
||||||
const assignmentExams = exams.filter(e => {
|
const assignmentExams = exams.filter((e) => {
|
||||||
const exam = findBy(assignment.exams, 'id', e.id)
|
const exam = findBy(assignment.exams, "id", e.id);
|
||||||
return !!exam && exam.module === e.module
|
return !!exam && exam.module === e.module;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (assignmentExams.every((x) => !!x)) {
|
if (assignmentExams.every((x) => !!x)) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "INIT_EXAM", payload: {
|
type: "INIT_EXAM",
|
||||||
|
payload: {
|
||||||
exams: assignmentExams.sort(sortByModule),
|
exams: assignmentExams.sort(sortByModule),
|
||||||
modules: mapBy(assignmentExams.sort(sortByModule), 'module'),
|
modules: mapBy(assignmentExams.sort(sortByModule), "module"),
|
||||||
assignment
|
assignment,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
router.push("/exam");
|
router.push("/exam");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const studentAssignments = useMemo(() => assignments.filter(activeAssignmentFilter), [assignments]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -116,9 +157,9 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
|
<div className="rounded-lg bg-neutral-200 px-2 py-1 ">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -127,20 +168,27 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
user={user}
|
user={user}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
icon: (
|
||||||
value: countFullExams(stats),
|
<BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
||||||
|
),
|
||||||
|
value: stats.fullExams,
|
||||||
label: "Exams",
|
label: "Exams",
|
||||||
tooltip: "Number of all conducted completed exams",
|
tooltip: "Number of all conducted completed exams",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
|
icon: (
|
||||||
value: countExamModules(stats),
|
<BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
||||||
|
),
|
||||||
|
value: stats.uniqueModules,
|
||||||
label: "Modules",
|
label: "Modules",
|
||||||
tooltip: "Number of all exam modules performed including Level Test",
|
tooltip:
|
||||||
|
"Number of all exam modules performed including Level Test",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BsStar className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />,
|
icon: (
|
||||||
value: `${stats.length > 0 ? averageScore(stats) : 0}%`,
|
<BsStar className="text-mti-red-light h-6 w-6 md:h-8 md:w-8" />
|
||||||
|
),
|
||||||
|
value: `${stats?.averageScore.toFixed(2) || 0}%`,
|
||||||
label: "Average Score",
|
label: "Average Score",
|
||||||
tooltip: "Average success rate for questions responded",
|
tooltip: "Average success rate for questions responded",
|
||||||
},
|
},
|
||||||
@@ -151,40 +199,50 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
<section className="flex flex-col gap-1 md:gap-3">
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
<span className="text-mti-black text-lg font-bold">Assignments</span>
|
<span className="text-mti-black text-lg font-bold">Assignments</span>
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
{studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."}
|
{assignments.length === 0 &&
|
||||||
{studentAssignments
|
"Assignments will appear here. It seems that for now there are no assignments for you."}
|
||||||
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
{assignments.map((assignment) => (
|
||||||
.map((assignment) => (
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
"border-mti-gray-anti-flash flex min-w-[350px] flex-col gap-6 rounded-xl border p-4",
|
||||||
assignment.results.map((r) => r.user).includes(user.id) && "border-mti-green-light",
|
assignment.hasResults && "border-mti-green-light"
|
||||||
)}
|
)}
|
||||||
key={assignment.id}>
|
key={assignment.id}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="text-mti-black/90 text-xl font-semibold">{assignment.name}</h3>
|
<h3 className="text-mti-black/90 text-xl font-semibold">
|
||||||
|
{assignment.name}
|
||||||
|
</h3>
|
||||||
<span className="flex justify-between gap-1 text-lg">
|
<span className="flex justify-between gap-1 text-lg">
|
||||||
<span>{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>
|
||||||
|
{moment(assignment.startDate).format("DD/MM/YY, HH:mm")}
|
||||||
|
</span>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<span>{moment(assignment.endDate).format("DD/MM/YY, HH:mm")}</span>
|
<span>
|
||||||
|
{moment(assignment.endDate).format("DD/MM/YY, HH:mm")}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="-md:mt-2 grid w-fit min-w-[140px] grid-cols-2 grid-rows-2 place-items-center justify-between gap-4">
|
<div className="-md:mt-2 grid w-fit min-w-[140px] grid-cols-2 grid-rows-2 place-items-center justify-between gap-4">
|
||||||
{assignment.exams
|
{assignment.exams.map((e) => (
|
||||||
.filter((e) => e.assignee === user.id)
|
<ModuleBadge
|
||||||
.map((e) => e.module)
|
className="scale-110 w-full"
|
||||||
.sort(sortByModuleName)
|
key={e.module}
|
||||||
.map((module) => (
|
module={e.module}
|
||||||
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!assignment.results.map((r) => r.user).includes(user.id) && (
|
{!assignment.hasResults && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
||||||
data-tip="Your screen size is too small to perform an assignment">
|
data-tip="Your screen size is too small to perform an assignment"
|
||||||
<Button className="h-full w-full !rounded-xl" variant="outline">
|
>
|
||||||
|
<Button
|
||||||
|
className="h-full w-full !rounded-xl"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,24 +250,33 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
data-tip="You have already started this assignment!"
|
data-tip="You have already started this assignment!"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
"-md:hidden h-full w-full max-w-[50%] cursor-pointer",
|
||||||
sessions.filter((x) => x.assignment?.id === assignment.id).length > 0 && "tooltip",
|
sessions.filter(
|
||||||
)}>
|
(x) => x.assignment?.id === assignment.id
|
||||||
|
).length > 0 && "tooltip"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
className={clsx("w-full h-full !rounded-xl")}
|
className={clsx("w-full h-full !rounded-xl")}
|
||||||
onClick={() => startAssignment(assignment)}
|
onClick={() => startAssignment(assignment)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={sessions.filter((x) => x.assignment?.id === assignment.id).length > 0}>
|
disabled={
|
||||||
|
sessions.filter(
|
||||||
|
(x) => x.assignment?.id === assignment.id
|
||||||
|
).length > 0
|
||||||
|
}
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{assignment.results.map((r) => r.user).includes(user.id) && (
|
{assignment.hasResults && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push("/record")}
|
onClick={() => router.push("/record")}
|
||||||
color="green"
|
color="green"
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
variant="outline">
|
variant="outline"
|
||||||
|
>
|
||||||
Submitted
|
Submitted
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -224,7 +291,11 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
<section className="flex flex-col gap-1 md:gap-3">
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
{invites.map((invite) => (
|
{invites.map((invite) => (
|
||||||
<InviteWithUserCard key={invite.id} invite={invite} reload={() => router.replace(router.asPath)} />
|
<InviteWithUserCard
|
||||||
|
key={invite.id}
|
||||||
|
invite={invite}
|
||||||
|
reload={() => router.replace(router.asPath)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
@@ -238,20 +309,41 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
const desiredLevel = user.desiredLevels[module] || 9;
|
const desiredLevel = user.desiredLevels[module] || 9;
|
||||||
const level = user.levels[module] || 0;
|
const level = user.levels[module] || 0;
|
||||||
return (
|
return (
|
||||||
<div className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4" key={module}>
|
<div
|
||||||
|
className="border-mti-gray-anti-flash flex flex-col gap-2 rounded-xl border p-4 w-full"
|
||||||
|
key={module}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2 md:gap-3">
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
|
<div className="bg-mti-gray-smoke flex h-8 w-8 items-center justify-center rounded-lg md:h-12 md:w-12 md:rounded-xl">
|
||||||
{module === "reading" && <BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />}
|
{module === "reading" && (
|
||||||
{module === "listening" && <BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />}
|
<BsBook className="text-ielts-reading h-4 w-4 md:h-5 md:w-5" />
|
||||||
{module === "writing" && <BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />}
|
)}
|
||||||
{module === "speaking" && <BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />}
|
{module === "listening" && (
|
||||||
{module === "level" && <BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />}
|
<BsHeadphones className="text-ielts-listening h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
|
{module === "writing" && (
|
||||||
|
<BsPen className="text-ielts-writing h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
|
{module === "speaking" && (
|
||||||
|
<BsMegaphone className="text-ielts-speaking h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
|
{module === "level" && (
|
||||||
|
<BsClipboard className="text-ielts-level h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<span className="text-sm font-bold md:font-extrabold">{capitalize(module)}</span>
|
<span className="text-sm font-bold md:font-extrabold">
|
||||||
|
{capitalize(module)}
|
||||||
|
</span>
|
||||||
<span className="text-mti-gray-dim text-sm font-normal">
|
<span className="text-mti-gray-dim text-sm font-normal">
|
||||||
{module === "level" && !!grading && `English Level: ${getGradingLabel(level, grading.steps)}`}
|
{module === "level" &&
|
||||||
{module !== "level" && `Level ${level} / Level 9 (Desired Level: ${desiredLevel})`}
|
!!grading &&
|
||||||
|
`English Level: ${getGradingLabel(
|
||||||
|
level,
|
||||||
|
grading.steps
|
||||||
|
)}`}
|
||||||
|
{module !== "level" &&
|
||||||
|
`Level ${level} / Level 9 (Desired Level: ${desiredLevel})`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,9 +351,17 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
<ProgressBar
|
<ProgressBar
|
||||||
color={module}
|
color={module}
|
||||||
label=""
|
label=""
|
||||||
mark={module === "level" ? undefined : Math.round((desiredLevel * 100) / 9)}
|
mark={
|
||||||
|
module === "level"
|
||||||
|
? undefined
|
||||||
|
: Math.round((desiredLevel * 100) / 9)
|
||||||
|
}
|
||||||
markLabel={`Desired Level: ${desiredLevel}`}
|
markLabel={`Desired Level: ${desiredLevel}`}
|
||||||
percentage={module === "level" ? level : Math.round((level * 100) / 9)}
|
percentage={
|
||||||
|
module === "level"
|
||||||
|
? level
|
||||||
|
: Math.round((level * 100) / 9)
|
||||||
|
}
|
||||||
className="h-2 w-full"
|
className="h-2 w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +370,7 @@ export default function Dashboard({ user, entities, assignments, stats, invites,
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import UserDisplayList from "@/components/UserDisplayList";
|
import UserDisplayList from "@/components/UserDisplayList";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
import { Module } from "@/interfaces";
|
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
import { Assignment } from "@/interfaces/results";
|
||||||
import { Group, Stat, User } from "@/interfaces/user";
|
import { Group, Stat, User } from "@/interfaces/user";
|
||||||
@@ -12,25 +10,27 @@ import { getEntitiesAssignments } from "@/utils/assignments.be";
|
|||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getGroupsByEntities } from "@/utils/groups.be";
|
import { getGroupsByEntities } from "@/utils/groups.be";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
import { calculateAverageLevel } from "@/utils/score";
|
||||||
import { groupByExam } from "@/utils/stats";
|
import { groupByExam } from "@/utils/stats";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
import { getStatsByUsers } from "@/utils/stats.be";
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { uniqBy } from "lodash";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import {
|
||||||
import { BsClipboard2Data, BsEnvelopePaper, BsPaperclip, BsPeople, BsPersonFill, BsPersonFillGear } from "react-icons/bs";
|
BsEnvelopePaper,
|
||||||
|
BsPeople,
|
||||||
|
BsPersonFill,
|
||||||
|
BsPersonFillGear,
|
||||||
|
} from "react-icons/bs";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
import { filterAllowedUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
users: User[];
|
students: User[];
|
||||||
entities: EntityWithRoles[];
|
entities: EntityWithRoles[];
|
||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
@@ -38,29 +38,68 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user || !user.isVerified) return redirect("/login")
|
if (!user || !user.isVerified) return redirect("/login");
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
if (!checkAccess(user, ["admin", "developer", "teacher"]))
|
||||||
return redirect("/")
|
return redirect("/");
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS);
|
|
||||||
const users = await filterAllowedUsers(user, entities)
|
const entities = await getEntitiesWithRoles(
|
||||||
|
isAdmin(user) ? undefined : entityIDS
|
||||||
|
);
|
||||||
|
|
||||||
|
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 assignments = await getEntitiesAssignments(entityIDS);
|
||||||
const stats = await getStatsByUsers(users.map((u) => u.id));
|
|
||||||
|
const stats = await getStatsByUsers(students.map((u) => u.id));
|
||||||
|
|
||||||
const groups = await getGroupsByEntities(entityIDS);
|
const groups = await getGroupsByEntities(entityIDS);
|
||||||
|
|
||||||
return { props: serialize({ user, users, entities, assignments, stats, groups }) };
|
return {
|
||||||
|
props: serialize({ user, students, entities, assignments, stats, groups }),
|
||||||
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
export default function Dashboard({ user, users, entities, assignments, stats, groups }: Props) {
|
export default function Dashboard({
|
||||||
const students = useMemo(() => users.filter((u) => u.type === "student"), [users]);
|
user,
|
||||||
|
students,
|
||||||
|
entities,
|
||||||
|
assignments,
|
||||||
|
stats,
|
||||||
|
groups,
|
||||||
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const allowedEntityStatistics = useAllowedEntities(user, entities, 'view_entity_statistics')
|
const allowedEntityStatistics = useAllowedEntities(
|
||||||
const allowedStudentPerformance = useAllowedEntities(user, entities, 'view_student_performance')
|
user,
|
||||||
|
entities,
|
||||||
|
"view_entity_statistics"
|
||||||
|
);
|
||||||
|
const allowedStudentPerformance = useAllowedEntities(
|
||||||
|
user,
|
||||||
|
entities,
|
||||||
|
"view_student_performance"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -74,7 +113,7 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="w-full flex flex-col gap-4">
|
<div className="w-full flex flex-col gap-4">
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
<div className="w-fit self-end bg-neutral-200 px-2 rounded-lg py-1">
|
||||||
@@ -97,7 +136,8 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
{allowedStudentPerformance.length > 0 && (
|
{allowedStudentPerformance.length > 0 && (
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/users/performance")}
|
onClick={() => router.push("/users/performance")}
|
||||||
label="Student Performance"
|
label="Student Performance"
|
||||||
value={students.length}
|
value={students.length}
|
||||||
@@ -105,7 +145,8 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allowedEntityStatistics.length > 0 && (
|
{allowedEntityStatistics.length > 0 && (
|
||||||
<IconCard Icon={BsPersonFillGear}
|
<IconCard
|
||||||
|
Icon={BsPersonFillGear}
|
||||||
onClick={() => router.push("/statistical")}
|
onClick={() => router.push("/statistical")}
|
||||||
label="Entity Statistics"
|
label="Entity Statistics"
|
||||||
value={allowedEntityStatistics.length}
|
value={allowedEntityStatistics.length}
|
||||||
@@ -124,26 +165,29 @@ export default function Dashboard({ user, users, entities, assignments, stats, g
|
|||||||
|
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-between">
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={students.sort((a, b) => dateSorter(a, b, "desc", "registrationDate"))}
|
users={students.sort((a, b) =>
|
||||||
|
dateSorter(a, b, "desc", "registrationDate")
|
||||||
|
)}
|
||||||
title="Latest Students"
|
title="Latest Students"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={students.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels))}
|
users={students.sort(
|
||||||
|
(a, b) =>
|
||||||
|
calculateAverageLevel(b.levels) -
|
||||||
|
calculateAverageLevel(a.levels)
|
||||||
|
)}
|
||||||
title="Highest level students"
|
title="Highest level students"
|
||||||
/>
|
/>
|
||||||
<UserDisplayList
|
<UserDisplayList
|
||||||
users={
|
users={students.sort(
|
||||||
students
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
Object.keys(groupByExam(filterBy(stats, "user", b))).length -
|
||||||
Object.keys(groupByExam(filterBy(stats, "user", a))).length,
|
Object.keys(groupByExam(filterBy(stats, "user", a))).length
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
title="Highest exam count students"
|
title="Highest exam count students"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
import { EntityWithRoles, Role } from "@/interfaces/entity";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import { User } from "@/interfaces/user";
|
||||||
import { Entity, EntityWithRoles, Role } from "@/interfaces/entity";
|
|
||||||
import { GroupWithUsers, User } from "@/interfaces/user";
|
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
import { findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntityWithRoles } from "@/utils/entities.be";
|
import { getEntityWithRoles } from "@/utils/entities.be";
|
||||||
import { convertToUsers, getGroup } from "@/utils/groups.be";
|
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
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 { 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 { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -28,7 +24,7 @@ import Head from "next/head";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Divider } from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactDatePicker from "react-datepicker";
|
import ReactDatePicker from "react-datepicker";
|
||||||
import { CURRENCIES } from "@/resources/paypal";
|
import { CURRENCIES } from "@/resources/paypal";
|
||||||
|
|
||||||
@@ -37,11 +33,9 @@ import {
|
|||||||
BsChevronLeft,
|
BsChevronLeft,
|
||||||
BsClockFill,
|
BsClockFill,
|
||||||
BsEnvelopeFill,
|
BsEnvelopeFill,
|
||||||
BsFillPersonVcardFill,
|
|
||||||
BsHash,
|
BsHash,
|
||||||
BsPerson,
|
BsPerson,
|
||||||
BsPlus,
|
BsPlus,
|
||||||
BsSquare,
|
|
||||||
BsStopwatchFill,
|
BsStopwatchFill,
|
||||||
BsTag,
|
BsTag,
|
||||||
BsTrash,
|
BsTrash,
|
||||||
@@ -344,7 +338,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@@ -555,7 +549,7 @@ export default function Home({ user, entity, users, linkedUsers }: Props) {
|
|||||||
searchFields={[["name"], ["email"], ["corporateInformation", "companyInformation", "name"], ["role", "label"], ["type"]]}
|
searchFields={[["name"], ["email"], ["corporateInformation", "companyInformation", "name"], ["role", "label"], ["type"]]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
@@ -152,7 +151,7 @@ interface Props {
|
|||||||
disableEdit?: boolean
|
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 [permissions, setPermissions] = useState(role.permissions)
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -240,7 +239,7 @@ export default function Role({ user, entity, role, userCount, disableEdit }: Pro
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
@@ -388,7 +387,7 @@ export default function Role({ user, entity, role, userCount, disableEdit }: Pro
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,25 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
|
||||||
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
import { useEntityPermission } from "@/hooks/useEntityPermissions";
|
||||||
import {useListSearch} from "@/hooks/useListSearch";
|
import { EntityWithRoles, Role} from "@/interfaces/entity";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import { User} from "@/interfaces/user";
|
||||||
import {Entity, EntityWithRoles, Role} from "@/interfaces/entity";
|
|
||||||
import {GroupWithUsers, User} from "@/interfaces/user";
|
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
|
||||||
import { redirect, serialize } from "@/utils";
|
import { redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import {getEntityWithRoles} from "@/utils/entities.be";
|
import {getEntityWithRoles} from "@/utils/entities.be";
|
||||||
import {convertToUsers, getGroup} from "@/utils/groups.be";
|
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import {checkAccess, doesEntityAllow, getTypesOfUser} from "@/utils/permissions";
|
import { doesEntityAllow} from "@/utils/permissions";
|
||||||
import {getUserName} from "@/utils/users";
|
import {getEntityUsers} from "@/utils/users.be";
|
||||||
import {getEntityUsers, getLinkedUsers, getSpecificUsers} from "@/utils/users.be";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import moment from "moment";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {Divider} from "primereact/divider";
|
import {Divider} from "primereact/divider";
|
||||||
import {useEffect, useMemo, useState} from "react";
|
|
||||||
import {
|
import {
|
||||||
BsChevronLeft,
|
BsChevronLeft,
|
||||||
BsClockFill,
|
|
||||||
BsEnvelopeFill,
|
|
||||||
BsFillPersonVcardFill,
|
|
||||||
BsPlus,
|
BsPlus,
|
||||||
BsSquare,
|
|
||||||
BsStopwatchFill,
|
|
||||||
BsTag,
|
|
||||||
BsTrash,
|
|
||||||
BsX,
|
|
||||||
} from "react-icons/bs";
|
} from "react-icons/bs";
|
||||||
import {toast, ToastContainer} from "react-toastify";
|
import {toast, ToastContainer} from "react-toastify";
|
||||||
|
|
||||||
@@ -133,7 +116,7 @@ export default function Home({user, entity, roles, users}: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
@@ -152,7 +135,7 @@ export default function Home({user, entity, roles, users}: Props) {
|
|||||||
|
|
||||||
<CardList list={roles} searchFields={[["label"]]} renderCard={renderCard} firstCard={canCreateRole ? firstCard : undefined} />
|
<CardList list={roles} searchFields={[["label"]]} renderCard={renderCard} firstCard={canCreateRole ? firstCard : undefined} />
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Select from "@/components/Low/Select";
|
|
||||||
import Tooltip from "@/components/Low/Tooltip";
|
import Tooltip from "@/components/Low/Tooltip";
|
||||||
import { useListSearch } from "@/hooks/useListSearch";
|
import { useListSearch } from "@/hooks/useListSearch";
|
||||||
import usePagination from "@/hooks/usePagination";
|
import usePagination from "@/hooks/usePagination";
|
||||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
import { Entity } from "@/interfaces/entity";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { redirect, serialize } from "@/utils";
|
||||||
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { getUserName } from "@/utils/users";
|
import { getUserName } from "@/utils/users";
|
||||||
import { getLinkedUsers, getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
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 { BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill } from "react-icons/bs";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { findAllowedEntities } from "@/utils/permissions";
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
@@ -91,7 +87,7 @@ export default function Home({ user, users }: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="flex flex-col gap-0">
|
<section className="flex flex-col gap-0">
|
||||||
<div className="flex gap-3 justify-between">
|
<div className="flex gap-3 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -178,7 +174,7 @@ export default function Home({ user, users }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ import Head from "next/head";
|
|||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
import { User } from "@/interfaces/user";
|
||||||
import { GroupWithUsers, User } from "@/interfaces/user";
|
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { getUserName, isAdmin } from "@/utils/users";
|
import { getUserName, isAdmin } from "@/utils/users";
|
||||||
import { convertToUsers, getGroupsForUser } from "@/utils/groups.be";
|
import { countEntityUsers, getEntityUsers } from "@/utils/users.be";
|
||||||
import { countEntityUsers, getEntityUsers, getSpecificUsers, getUsers } from "@/utils/users.be";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
import { checkAccess, findAllowedEntities, getTypesOfUser } from "@/utils/permissions";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { uniq } from "lodash";
|
|
||||||
import { BsBank, BsPlus } from "react-icons/bs";
|
import { BsBank, BsPlus } from "react-icons/bs";
|
||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
@@ -20,24 +17,34 @@ import Separator from "@/components/Low/Separator";
|
|||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
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 }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
const entityIDs = mapBy(user.entities, 'id')
|
const entityIDs = mapBy(user.entities, "id");
|
||||||
const entities = await getEntitiesWithRoles(["admin", "developer"].includes(user.type) ? undefined : entityIDs);
|
const entities = await getEntitiesWithRoles(
|
||||||
const allowedEntities = findAllowedEntities(user, entities, 'view_entities')
|
["admin", "developer"].includes(user.type) ? undefined : entityIDs
|
||||||
|
);
|
||||||
|
const allowedEntities = findAllowedEntities(user, entities, "view_entities");
|
||||||
|
|
||||||
const entitiesWithCount = await Promise.all(
|
const entitiesWithCount = await Promise.all(
|
||||||
allowedEntities.map(async (e) => ({
|
allowedEntities.map(async (e) => ({
|
||||||
entity: e,
|
entity: e,
|
||||||
count: await countEntityUsers(e.id, { type: { $in: ["student", "teacher", "corporate", "mastercorporate"] } }),
|
count: await countEntityUsers(e.id, {
|
||||||
users: await getEntityUsers(e.id, 5, { type: { $in: ["student", "teacher", "corporate", "mastercorporate"] } })
|
type: { $in: ["student", "teacher", "corporate", "mastercorporate"] },
|
||||||
})),
|
}),
|
||||||
|
users: await getEntityUsers(e.id, 5, {
|
||||||
|
type: { $in: ["student", "teacher", "corporate", "mastercorporate"] },
|
||||||
|
}),
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -56,19 +63,33 @@ export default function Home({ user, entities }: Props) {
|
|||||||
<Link
|
<Link
|
||||||
href={`/entities/${entity.id}`}
|
href={`/entities/${entity.id}`}
|
||||||
key={entity.id}
|
key={entity.id}
|
||||||
className="p-4 border-2 border-mti-purple-light/20 rounded-xl flex gap-2 justify-between hover:border-mti-purple group transition ease-in-out duration-300 text-left cursor-pointer">
|
className="p-4 border-2 border-mti-purple-light/20 rounded-xl flex gap-2 justify-between hover:border-mti-purple group transition ease-in-out duration-300 text-left cursor-pointer"
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Entity</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
|
Entity
|
||||||
|
</span>
|
||||||
{entity.label}
|
{entity.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className="bg-mti-purple text-white font-semibold px-2">Members</span>
|
<span className="bg-mti-purple text-white font-semibold px-2">
|
||||||
<span className="bg-mti-purple-light/50 px-2">{count}{isAdmin(user) && ` / ${entity.licenses || 0}`}</span>
|
Members
|
||||||
|
</span>
|
||||||
|
<span className="bg-mti-purple-light/50 px-2">
|
||||||
|
{count}
|
||||||
|
{isAdmin(user) && ` / ${entity.licenses || 0}`}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{users.map(getUserName).join(", ")}{' '}
|
{users.map(getUserName).join(", ")}{" "}
|
||||||
{count > 5 ? <span className="opacity-50 bg-mti-purple-light/50 px-1 text-sm">and {count - 5} more</span> : ""}
|
{count > 5 ? (
|
||||||
|
<span className="opacity-50 bg-mti-purple-light/50 px-1 text-sm">
|
||||||
|
and {count - 5} more
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
@@ -80,7 +101,8 @@ export default function Home({ user, entities }: Props) {
|
|||||||
const firstCard = () => (
|
const firstCard = () => (
|
||||||
<Link
|
<Link
|
||||||
href={`/entities/create`}
|
href={`/entities/create`}
|
||||||
className="p-4 border-2 hover:text-mti-purple rounded-xl flex flex-col items-center justify-center gap-0 hover:border-mti-purple transition ease-in-out duration-300 text-left cursor-pointer">
|
className="p-4 border-2 hover:text-mti-purple rounded-xl flex flex-col items-center justify-center gap-0 hover:border-mti-purple transition ease-in-out duration-300 text-left cursor-pointer"
|
||||||
|
>
|
||||||
<BsPlus size={40} />
|
<BsPlus size={40} />
|
||||||
<span className="font-semibold">Create Entity</span>
|
<span className="font-semibold">Create Entity</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -98,7 +120,7 @@ export default function Home({ user, entities }: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user} className="!gap-4">
|
<>
|
||||||
<section className="flex flex-col gap-4 w-full h-full">
|
<section className="flex flex-col gap-4 w-full h-full">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h2 className="font-bold text-2xl">Entities</h2>
|
<h2 className="font-bold text-2xl">Entities</h2>
|
||||||
@@ -109,10 +131,12 @@ export default function Home({ user, entities }: Props) {
|
|||||||
list={entities}
|
list={entities}
|
||||||
searchFields={SEARCH_FIELDS}
|
searchFields={SEARCH_FIELDS}
|
||||||
renderCard={renderCard}
|
renderCard={renderCard}
|
||||||
firstCard={["admin", "developer"].includes(user.type) ? firstCard : undefined}
|
firstCard={
|
||||||
|
["admin", "developer"].includes(user.type) ? firstCard : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { useRouter } from "next/router";
|
|||||||
import { getSessionByAssignment } from "@/utils/sessions.be";
|
import { getSessionByAssignment } from "@/utils/sessions.be";
|
||||||
import { Session } from "@/hooks/useSessions";
|
import { Session } from "@/hooks/useSessions";
|
||||||
import { activeAssignmentFilter } from "@/utils/assignments";
|
import { activeAssignmentFilter } from "@/utils/assignments";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
|
|||||||
@@ -2,24 +2,22 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { Radio, RadioGroup } from "@headlessui/react";
|
import { Radio, RadioGroup } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import ExamEditorStore from "@/stores/examEditor/types";
|
import ExamEditorStore from "@/stores/examEditor/types";
|
||||||
import ExamEditor from "@/components/ExamEditor";
|
import ExamEditor from "@/components/ExamEditor";
|
||||||
import MultipleAudioUploader from "@/components/ExamEditor/Shared/AudioEdit";
|
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { Module } from "@/interfaces";
|
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 { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
@@ -157,7 +155,7 @@ export default function Generation({ id, user, exam, examModule, permissions }:
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user} className="gap-6">
|
<>
|
||||||
<h1 className="text-2xl font-semibold">Exam Editor</h1>
|
<h1 className="text-2xl font-semibold">Exam Editor</h1>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Input
|
<Input
|
||||||
@@ -212,7 +210,7 @@ export default function Generation({ id, user, exam, examModule, permissions }:
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<ExamEditor levelParts={examLevelParts} />
|
<ExamEditor levelParts={examLevelParts} />
|
||||||
</Layout>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import AssignmentCard from "@/components/High/AssignmentCard";
|
import AssignmentCard from "@/components/High/AssignmentCard";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
@@ -15,7 +14,10 @@ import { sessionOptions } from "@/lib/session";
|
|||||||
import useExamStore from "@/stores/exam";
|
import useExamStore from "@/stores/exam";
|
||||||
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments";
|
import {
|
||||||
|
activeAssignmentFilter,
|
||||||
|
futureAssignmentFilter,
|
||||||
|
} from "@/utils/assignments";
|
||||||
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getExamsByIds } from "@/utils/exams.be";
|
import { getExamsByIds } from "@/utils/exams.be";
|
||||||
@@ -45,66 +47,87 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
const destination = Buffer.from(req.url || "/").toString("base64")
|
const destination = Buffer.from(req.url || "/").toString("base64");
|
||||||
if (!user) return redirect(`/login?destination=${destination}`)
|
if (!user) return redirect(`/login?destination=${destination}`);
|
||||||
|
|
||||||
if (!checkAccess(user, ["admin", "developer", "student"]))
|
if (!checkAccess(user, ["admin", "developer", "student"]))
|
||||||
return redirect("/")
|
return redirect("/");
|
||||||
|
|
||||||
const entityIDS = mapBy(user.entities, "id") || [];
|
const entityIDS = mapBy(user.entities, "id") || [];
|
||||||
|
|
||||||
const entities = await getEntitiesWithRoles(entityIDS);
|
const entities = await getEntitiesWithRoles(entityIDS);
|
||||||
const assignments = await getAssignmentsByAssignee(user.id, { archived: { $ne: true } });
|
const assignments = await getAssignmentsByAssignee(user.id, {
|
||||||
const sessions = await getSessionsByUser(user.id, 0, { "assignment.id": { $in: mapBy(assignments, 'id') } });
|
archived: { $ne: true },
|
||||||
|
});
|
||||||
|
const sessions = await getSessionsByUser(user.id, 0, {
|
||||||
|
"assignment.id": { $in: mapBy(assignments, "id") },
|
||||||
|
});
|
||||||
|
|
||||||
const examIDs = uniqBy(
|
const examIDs = uniqBy(
|
||||||
assignments.flatMap((a) =>
|
assignments.flatMap((a) =>
|
||||||
filterBy(a.exams, 'assignee', user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })),
|
filterBy(a.exams, "assignee", user.id).map(
|
||||||
|
(e: any) => ({
|
||||||
|
module: e.module,
|
||||||
|
id: e.id,
|
||||||
|
key: `${e.module}_${e.id}`,
|
||||||
|
})
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"key",
|
"key"
|
||||||
);
|
);
|
||||||
const exams = await getExamsByIds(examIDs);
|
const exams = await getExamsByIds(examIDs);
|
||||||
|
|
||||||
return { props: serialize({ user, entities, assignments, exams, sessions }) };
|
return { props: serialize({ user, entities, assignments, exams, sessions }) };
|
||||||
}, sessionOptions);
|
}, 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) {
|
export default function OfficialExam({
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
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 = () => {
|
const reload = () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
router.replace(router.asPath)
|
router.replace(router.asPath);
|
||||||
setTimeout(() => setIsLoading(false), 500)
|
setTimeout(() => setIsLoading(false), 500);
|
||||||
}
|
};
|
||||||
|
|
||||||
const startAssignment = (assignment: Assignment) => {
|
const startAssignment = (assignment: Assignment) => {
|
||||||
const assignmentExams = exams.filter(e => {
|
const assignmentExams = exams.filter((e) => {
|
||||||
const exam = findBy(assignment.exams, 'id', e.id)
|
const exam = findBy(assignment.exams, "id", e.id);
|
||||||
return !!exam && exam.module === e.module
|
return !!exam && exam.module === e.module;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (assignmentExams.every((x) => !!x)) {
|
if (assignmentExams.every((x) => !!x)) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "INIT_EXAM", payload: {
|
type: "INIT_EXAM",
|
||||||
|
payload: {
|
||||||
exams: assignmentExams.sort(sortByModule),
|
exams: assignmentExams.sort(sortByModule),
|
||||||
modules: mapBy(assignmentExams.sort(sortByModule), 'module'),
|
modules: mapBy(assignmentExams.sort(sortByModule), "module"),
|
||||||
assignment
|
assignment,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
router.push(`/exam?assignment=${assignment.id}&destination=${destination}`);
|
router.push(
|
||||||
|
`/exam?assignment=${assignment.id}&destination=${destination}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSession = async (session: Session) => {
|
const loadSession = async (session: Session) => {
|
||||||
dispatch({ type: "SET_SESSION", payload: { session } });
|
dispatch({ type: "SET_SESSION", payload: { session } });
|
||||||
router.push(`/exam?assignment=${session.assignment?.id}&destination=${destination}`);
|
router.push(
|
||||||
|
`/exam?assignment=${session.assignment?.id}&destination=${destination}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
@@ -113,12 +136,21 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const studentAssignments = useMemo(() => [
|
const studentAssignments = useMemo(
|
||||||
...assignments.filter(activeAssignmentFilter), ...assignments.filter(futureAssignmentFilter)],
|
() => [
|
||||||
|
...assignments.filter(activeAssignmentFilter),
|
||||||
|
...assignments.filter(futureAssignmentFilter),
|
||||||
|
],
|
||||||
[assignments]
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -132,7 +164,7 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user} hideSidebar>
|
<>
|
||||||
{entities.length > 0 && (
|
{entities.length > 0 && (
|
||||||
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
|
<div className="absolute right-4 top-4 rounded-lg bg-neutral-200 px-2 py-1">
|
||||||
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
<b>{mapBy(entities, "label")?.join(", ")}</b>
|
||||||
@@ -147,29 +179,44 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
|
|||||||
<section className="flex flex-col gap-1 md:gap-3">
|
<section className="flex flex-col gap-1 md:gap-3">
|
||||||
<div
|
<div
|
||||||
onClick={reload}
|
onClick={reload}
|
||||||
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out">
|
className="text-mti-purple-light hover:text-mti-purple-dark flex cursor-pointer items-center gap-2 transition duration-300 ease-in-out"
|
||||||
<span className="text-mti-black text-lg font-bold">Assignments</span>
|
>
|
||||||
<BsArrowRepeat className={clsx("text-xl", isLoading && "animate-spin")} />
|
<span className="text-mti-black text-lg font-bold">
|
||||||
|
Assignments
|
||||||
|
</span>
|
||||||
|
<BsArrowRepeat
|
||||||
|
className={clsx("text-xl", isLoading && "animate-spin")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
<span className="text-mti-gray-taupe scrollbar-hide flex gap-8 overflow-x-scroll">
|
||||||
{studentAssignments.length === 0 && "Assignments will appear here. It seems that for now there are no assignments for you."}
|
{studentAssignments.length === 0 &&
|
||||||
|
"Assignments will appear here. It seems that for now there are no assignments for you."}
|
||||||
{studentAssignments
|
{studentAssignments
|
||||||
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
.sort((a, b) => moment(a.startDate).diff(b.startDate))
|
||||||
.map((a) =>
|
.map((a) => (
|
||||||
<AssignmentCard
|
<AssignmentCard
|
||||||
key={a.id}
|
key={a.id}
|
||||||
assignment={a}
|
assignment={a}
|
||||||
user={user}
|
user={user}
|
||||||
session={assignmentSessions.find(s => s.assignment?.id === a.id)}
|
session={assignmentSessions.find(
|
||||||
|
(s) => s.assignment?.id === a.id
|
||||||
|
)}
|
||||||
startAssignment={startAssignment}
|
startAssignment={startAssignment}
|
||||||
resumeAssignment={loadSession}
|
resumeAssignment={loadSession}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Button onClick={logout} variant="outline" color="red" className="max-w-[200px] w-full absolute bottom-8 left-8">Sign out</Button>
|
<Button
|
||||||
</Layout>
|
onClick={logout}
|
||||||
|
variant="outline"
|
||||||
|
color="red"
|
||||||
|
className="max-w-[200px] w-full absolute bottom-8 left-8"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import useUser from "@/hooks/useUser";
|
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import usePayments from "@/hooks/usePayments";
|
import usePayments from "@/hooks/usePayments";
|
||||||
import usePaypalPayments from "@/hooks/usePaypalPayments";
|
import usePaypalPayments from "@/hooks/usePaypalPayments";
|
||||||
import { Payment, PaypalPayment } from "@/interfaces/paypal";
|
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 { CURRENCIES } from "@/resources/paypal";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -943,7 +941,7 @@ export default function PaymentRecord({ user, entities }: Props) {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user} className="gap-6">
|
<>
|
||||||
{getUserModal()}
|
{getUserModal()}
|
||||||
<Modal isOpen={isCreatingPayment} onClose={() => setIsCreatingPayment(false)}>
|
<Modal isOpen={isCreatingPayment} onClose={() => setIsCreatingPayment(false)}>
|
||||||
<PaymentCreator
|
<PaymentCreator
|
||||||
@@ -1248,7 +1246,7 @@ export default function PaymentRecord({ user, entities }: Props) {
|
|||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</Layout>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {useEffect, useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { Permission, PermissionType } from "@/interfaces/permissions";
|
import { Permission, PermissionType } from "@/interfaces/permissions";
|
||||||
import { getPermissionDoc } from "@/utils/permissions.be";
|
import { getPermissionDoc } from "@/utils/permissions.be";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import Layout from "@/components/High/Layout";
|
import { LayoutContext } from "@/components/High/Layout";
|
||||||
import { getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
@@ -30,13 +30,14 @@ interface PermissionWithBasicUsers {
|
|||||||
users: BasicUser[];
|
users: BasicUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => {
|
export const getServerSideProps = withIronSessionSsr(
|
||||||
const user = await requestUser(req, res)
|
async ({ req, res, params }) => {
|
||||||
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("/");
|
||||||
|
|
||||||
if (!params?.id) return redirect("/permissions")
|
if (!params?.id) return redirect("/permissions");
|
||||||
|
|
||||||
// Fetch data from external API
|
// Fetch data from external API
|
||||||
const permission: Permission = await getPermissionDoc(params.id as string);
|
const permission: Permission = await getPermissionDoc(params.id as string);
|
||||||
@@ -49,7 +50,9 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
|
|||||||
user.type === "corporate"
|
user.type === "corporate"
|
||||||
? userGroups
|
? userGroups
|
||||||
: user.type === "mastercorporate"
|
: user.type === "mastercorporate"
|
||||||
? groups.filter((x) => userGroups.flatMap((y) => y.participants).includes(x.admin))
|
? groups.filter((x) =>
|
||||||
|
userGroups.flatMap((y) => y.participants).includes(x.admin)
|
||||||
|
)
|
||||||
: groups;
|
: groups;
|
||||||
|
|
||||||
const users = allUserData.map((u) => ({
|
const users = allUserData.map((u) => ({
|
||||||
@@ -59,17 +62,22 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
|
|||||||
})) as BasicUser[];
|
})) as BasicUser[];
|
||||||
|
|
||||||
const filteredUsers = ["mastercorporate", "corporate"].includes(user.type)
|
const filteredUsers = ["mastercorporate", "corporate"].includes(user.type)
|
||||||
? users.filter((u) => filteredGroups.flatMap((g) => g.participants).includes(u.id))
|
? users.filter((u) =>
|
||||||
|
filteredGroups.flatMap((g) => g.participants).includes(u.id)
|
||||||
|
)
|
||||||
: users;
|
: users;
|
||||||
|
|
||||||
// const res = await fetch("api/permissions");
|
// const res = await fetch("api/permissions");
|
||||||
// const permissions: Permission[] = await res.json();
|
// const permissions: Permission[] = await res.json();
|
||||||
// Pass data to the page via props
|
// Pass data to the page via props
|
||||||
const usersData: BasicUser[] = permission.users.reduce((acc: BasicUser[], userId) => {
|
const usersData: BasicUser[] = permission.users.reduce(
|
||||||
|
(acc: BasicUser[], userId) => {
|
||||||
const user = filteredUsers.find((u) => u.id === userId) as BasicUser;
|
const user = filteredUsers.find((u) => u.id === userId) as BasicUser;
|
||||||
if (!!user) acc.push(user);
|
if (!!user) acc.push(user);
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
@@ -83,7 +91,9 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
|
|||||||
users: filteredUsers,
|
users: filteredUsers,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
},
|
||||||
|
sessionOptions
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
permission: PermissionWithBasicUsers;
|
permission: PermissionWithBasicUsers;
|
||||||
@@ -94,7 +104,9 @@ interface Props {
|
|||||||
export default function Page(props: Props) {
|
export default function Page(props: Props) {
|
||||||
const { permission, user, users } = props;
|
const { permission, user, users } = props;
|
||||||
|
|
||||||
const [selectedUsers, setSelectedUsers] = useState<string[]>(() => permission.users.map((u) => u.id));
|
const [selectedUsers, setSelectedUsers] = useState<string[]>(() =>
|
||||||
|
permission.users.map((u) => u.id)
|
||||||
|
);
|
||||||
|
|
||||||
const onChange = (value: any) => {
|
const onChange = (value: any) => {
|
||||||
setSelectedUsers((prev) => {
|
setSelectedUsers((prev) => {
|
||||||
@@ -119,6 +131,13 @@ export default function Page(props: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { setClassName } = React.useContext(LayoutContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setClassName("gap-6");
|
||||||
|
return () => setClassName("");
|
||||||
|
}, [setClassName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -131,9 +150,11 @@ export default function Page(props: Props) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user} className="gap-6">
|
<>
|
||||||
<div className="flex flex-col gap-6 w-full h-[88vh] overflow-y-scroll scrollbar-hide rounded-xl">
|
<div className="flex flex-col gap-6 w-full h-[88vh] overflow-y-scroll scrollbar-hide rounded-xl">
|
||||||
<h1 className="text-2xl font-semibold">Permission: {permission.type as string}</h1>
|
<h1 className="text-2xl font-semibold">
|
||||||
|
Permission: {permission.type as string}
|
||||||
|
</h1>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Select
|
<Select
|
||||||
value={null}
|
value={null}
|
||||||
@@ -154,11 +175,18 @@ export default function Page(props: Props) {
|
|||||||
{selectedUsers.map((userId) => {
|
{selectedUsers.map((userId) => {
|
||||||
const user = users.find((u) => u.id === userId);
|
const user = users.find((u) => u.id === userId);
|
||||||
return (
|
return (
|
||||||
<div className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" key={userId}>
|
<div
|
||||||
|
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4"
|
||||||
|
key={userId}
|
||||||
|
>
|
||||||
<span className="text-base first-letter:uppercase">
|
<span className="text-base first-letter:uppercase">
|
||||||
{user?.type}-{user?.name}
|
{user?.type}-{user?.name}
|
||||||
</span>
|
</span>
|
||||||
<BsTrash style={{cursor: "pointer"}} onClick={() => removeUser(userId)} size={20} />
|
<BsTrash
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={() => removeUser(userId)}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -171,7 +199,10 @@ export default function Page(props: Props) {
|
|||||||
.filter((user) => !selectedUsers.includes(user.id))
|
.filter((user) => !selectedUsers.includes(user.id))
|
||||||
.map((user) => {
|
.map((user) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4" key={user.id}>
|
<div
|
||||||
|
className="flex p-4 rounded-xl w-auto bg-mti-purple-light text-white gap-4"
|
||||||
|
key={user.id}
|
||||||
|
>
|
||||||
<span className="text-base first-letter:uppercase">
|
<span className="text-base first-letter:uppercase">
|
||||||
{user?.type}-{user?.name}
|
{user?.type}-{user?.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -182,7 +213,7 @@ export default function Page(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,33 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
|||||||
import { Permission } from "@/interfaces/permissions";
|
import { Permission } from "@/interfaces/permissions";
|
||||||
import { getPermissionDocs } from "@/utils/permissions.be";
|
import { getPermissionDocs } from "@/utils/permissions.be";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import Layout from "@/components/High/Layout";
|
import { LayoutContext } from "@/components/High/Layout";
|
||||||
import PermissionList from "@/components/PermissionList";
|
import PermissionList from "@/components/PermissionList";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { redirect } from "@/utils";
|
import { redirect } from "@/utils";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login");
|
||||||
|
|
||||||
if (shouldRedirectHome(user)) return redirect("/")
|
if (shouldRedirectHome(user)) return redirect("/");
|
||||||
|
|
||||||
// Fetch data from external API
|
// Fetch data from external API
|
||||||
const permissions: Permission[] = await getPermissionDocs();
|
const permissions: Permission[] = await getPermissionDocs();
|
||||||
const filteredPermissions = permissions.filter((p) => {
|
const filteredPermissions = permissions.filter((p) => {
|
||||||
const permissionType = p.type.toString().toLowerCase();
|
const permissionType = p.type.toString().toLowerCase();
|
||||||
|
|
||||||
if (user.type === "corporate") return !permissionType.includes("corporate") && !permissionType.includes("admin");
|
if (user.type === "corporate")
|
||||||
if (user.type === "mastercorporate") return !permissionType.includes("mastercorporate") && !permissionType.includes("admin");
|
return (
|
||||||
|
!permissionType.includes("corporate") &&
|
||||||
|
!permissionType.includes("admin")
|
||||||
|
);
|
||||||
|
if (user.type === "mastercorporate")
|
||||||
|
return (
|
||||||
|
!permissionType.includes("mastercorporate") &&
|
||||||
|
!permissionType.includes("admin")
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -51,6 +60,9 @@ interface Props {
|
|||||||
export default function Page(props: Props) {
|
export default function Page(props: Props) {
|
||||||
const { permissions, user } = props;
|
const { permissions, user } = props;
|
||||||
|
|
||||||
|
const { setClassName } = React.useContext(LayoutContext);
|
||||||
|
React.useEffect(() => setClassName("gap-6"), [setClassName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -62,12 +74,12 @@ export default function Page(props: Props) {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout user={user} className="gap-6">
|
<>
|
||||||
<h1 className="text-2xl font-semibold">Permissions</h1>
|
<h1 className="text-2xl font-semibold">Permissions</h1>
|
||||||
<div className="flex gap-3 flex-wrap overflow-y-scroll scrollbar-hide h-[80vh] rounded-xl">
|
<div className="flex gap-3 flex-wrap overflow-y-scroll scrollbar-hide h-[80vh] rounded-xl">
|
||||||
<PermissionList permissions={permissions} />
|
<PermissionList permissions={permissions} />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
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 useUser from "@/hooks/useUser";
|
||||||
import { toast, ToastContainer } from "react-toastify";
|
import { toast, ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -15,26 +21,21 @@ import clsx from "clsx";
|
|||||||
import {
|
import {
|
||||||
CorporateUser,
|
CorporateUser,
|
||||||
EmploymentStatus,
|
EmploymentStatus,
|
||||||
EMPLOYMENT_STATUS,
|
|
||||||
Gender,
|
Gender,
|
||||||
User,
|
User,
|
||||||
DemographicInformation,
|
DemographicInformation,
|
||||||
MasterCorporateUser,
|
MasterCorporateUser,
|
||||||
Group,
|
|
||||||
} from "@/interfaces/user";
|
} from "@/interfaces/user";
|
||||||
import CountrySelect from "@/components/Low/CountrySelect";
|
import CountrySelect from "@/components/Low/CountrySelect";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { BsCamera, BsQuestionCircleFill } from "react-icons/bs";
|
import { BsCamera, BsQuestionCircleFill } from "react-icons/bs";
|
||||||
import { USER_TYPE_LABELS } from "@/resources/user";
|
import { USER_TYPE_LABELS } from "@/resources/user";
|
||||||
import useGroups from "@/hooks/useGroups";
|
|
||||||
import useUsers from "@/hooks/useUsers";
|
|
||||||
import { convertBase64, redirect } from "@/utils";
|
import { convertBase64, redirect } from "@/utils";
|
||||||
import { Divider } from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import GenderInput from "@/components/High/GenderInput";
|
import GenderInput from "@/components/High/GenderInput";
|
||||||
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
import EmploymentStatusInput from "@/components/High/EmploymentStatusInput";
|
||||||
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
import TimezoneSelect from "@/components/Low/TImezoneSelect";
|
||||||
import Modal from "@/components/Modal";
|
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
@@ -42,39 +43,65 @@ import { InstructorGender } from "@/interfaces/exam";
|
|||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import TopicModal from "@/components/Medium/TopicModal";
|
import TopicModal from "@/components/Medium/TopicModal";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { getParticipantGroups, getUserCorporate } from "@/utils/groups.be";
|
import { getParticipantGroups, getUserCorporate } from "@/utils/groups.be";
|
||||||
import { InferGetServerSidePropsType } from "next";
|
import { countUsers, getUser } from "@/utils/users.be";
|
||||||
import { getUsers } from "@/utils/users.be";
|
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res);
|
||||||
if (!user) return redirect("/login")
|
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;
|
||||||
|
|
||||||
|
const hasBenefitsFromUniversity =
|
||||||
|
(await countUsers({
|
||||||
|
id: { $in: groups },
|
||||||
|
type: "corporate",
|
||||||
|
})) > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
linkedCorporate: (await getUserCorporate(user.id)) || null,
|
linkedCorporate,
|
||||||
groups: await getParticipantGroups(user.id),
|
hasBenefitsFromUniversity,
|
||||||
users: await getUsers(),
|
referralAgent,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
groups: Group[];
|
hasBenefitsFromUniversity: boolean;
|
||||||
users: User[];
|
|
||||||
mutateUser: Function;
|
mutateUser: Function;
|
||||||
|
referralAgent?: User;
|
||||||
linkedCorporate?: CorporateUser | MasterCorporateUser;
|
linkedCorporate?: CorporateUser | MasterCorporateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DoubleColumnRow = ({ children }: { children: ReactNode }) => <div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>;
|
const DoubleColumnRow = ({ children }: { children: ReactNode }) => (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8 w-full">{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props) {
|
function UserProfile({
|
||||||
|
user,
|
||||||
|
mutateUser,
|
||||||
|
linkedCorporate,
|
||||||
|
hasBenefitsFromUniversity,
|
||||||
|
referralAgent,
|
||||||
|
}: Props) {
|
||||||
const [bio, setBio] = useState(user.bio || "");
|
const [bio, setBio] = useState(user.bio || "");
|
||||||
const [name, setName] = useState(user.name || "");
|
const [name, setName] = useState(user.name || "");
|
||||||
const [email, setEmail] = useState(user.email || "");
|
const [email, setEmail] = useState(user.email || "");
|
||||||
@@ -83,37 +110,69 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
const [profilePicture, setProfilePicture] = useState(user.profilePicture);
|
||||||
|
|
||||||
const [desiredLevels, setDesiredLevels] = useState(checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined);
|
const [desiredLevels, setDesiredLevels] = useState(
|
||||||
|
checkAccess(user, ["developer", "student"]) ? user.desiredLevels : undefined
|
||||||
|
);
|
||||||
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
|
const [focus, setFocus] = useState<"academic" | "general">(user.focus);
|
||||||
|
|
||||||
const [country, setCountry] = useState<string>(user.demographicInformation?.country || "");
|
const [country, setCountry] = useState<string>(
|
||||||
const [phone, setPhone] = useState<string>(user.demographicInformation?.phone || "");
|
user.demographicInformation?.country || ""
|
||||||
const [gender, setGender] = useState<Gender | undefined>(user.demographicInformation?.gender || undefined);
|
);
|
||||||
|
const [phone, setPhone] = useState<string>(
|
||||||
|
user.demographicInformation?.phone || ""
|
||||||
|
);
|
||||||
|
const [gender, setGender] = useState<Gender | undefined>(
|
||||||
|
user.demographicInformation?.gender || undefined
|
||||||
|
);
|
||||||
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
const [employment, setEmployment] = useState<EmploymentStatus | undefined>(
|
||||||
checkAccess(user, ["corporate", "mastercorporate"]) ? undefined : (user.demographicInformation as DemographicInformation)?.employment,
|
checkAccess(user, ["corporate", "mastercorporate"])
|
||||||
|
? undefined
|
||||||
|
: (user.demographicInformation as DemographicInformation)?.employment
|
||||||
);
|
);
|
||||||
const [passport_id, setPassportID] = useState<string | undefined>(
|
const [passport_id, setPassportID] = useState<string | undefined>(
|
||||||
checkAccess(user, ["student"]) ? (user.demographicInformation as DemographicInformation)?.passport_id : undefined,
|
checkAccess(user, ["student"])
|
||||||
|
? (user.demographicInformation as DemographicInformation)?.passport_id
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const [preferredGender, setPreferredGender] = useState<InstructorGender | undefined>(
|
const [preferredGender, setPreferredGender] = useState<
|
||||||
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
|
InstructorGender | undefined
|
||||||
|
>(
|
||||||
|
user.type === "student" || user.type === "developer"
|
||||||
|
? user.preferredGender || "varied"
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
|
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
|
||||||
user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
|
user.type === "student" || user.type === "developer"
|
||||||
|
? user.preferredTopics
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const [position, setPosition] = useState<string | undefined>(
|
const [position, setPosition] = useState<string | undefined>(
|
||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.demographicInformation?.position : undefined,
|
user.type === "corporate" || user.type === "mastercorporate"
|
||||||
|
? user.demographicInformation?.position
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
const [corporateInformation, setCorporateInformation] = useState(
|
const [corporateInformation, setCorporateInformation] = useState(
|
||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation : undefined,
|
user.type === "corporate" || user.type === "mastercorporate"
|
||||||
|
? user.corporateInformation
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const [companyName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyName : undefined);
|
const [companyName] = useState<string | undefined>(
|
||||||
const [commercialRegistration] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined);
|
user.type === "agent" ? user.agentInformation?.companyName : undefined
|
||||||
const [arabName, setArabName] = useState<string | undefined>(user.type === "agent" ? user.agentInformation?.companyArabName : undefined);
|
);
|
||||||
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
|
const [commercialRegistration] = useState<string | undefined>(
|
||||||
|
user.type === "agent"
|
||||||
|
? user.agentInformation?.commercialRegistration
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
const [arabName, setArabName] = useState<string | undefined>(
|
||||||
|
user.type === "agent" ? user.agentInformation?.companyArabName : undefined
|
||||||
|
);
|
||||||
|
const [timezone, setTimezone] = useState<string>(
|
||||||
|
user.demographicInformation?.timezone || moment.tz.guess()
|
||||||
|
);
|
||||||
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
|
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
|
||||||
|
|
||||||
const profilePictureInput = useRef(null);
|
const profilePictureInput = useRef(null);
|
||||||
@@ -121,9 +180,12 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
const momentDate = moment(date);
|
const momentDate = moment(date);
|
||||||
const today = moment(new 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(1, "days").isAfter(momentDate))
|
||||||
if (today.add(3, "days").isAfter(momentDate)) return "!bg-mti-rose-ultralight border-mti-rose-light";
|
return "!bg-mti-red-ultralight border-mti-red-light";
|
||||||
if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-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<HTMLInputElement>) => {
|
const uploadProfilePicture = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -143,15 +205,15 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword && !password) {
|
if (newPassword && !password) {
|
||||||
toast.error("To update your password you need to input your current one!");
|
toast.error(
|
||||||
|
"To update your password you need to input your current one!"
|
||||||
|
);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email !== user?.email) {
|
if (email !== user?.email) {
|
||||||
const userAdmins = groups.filter((x) => x.participants.includes(user.id)).map((x) => x.admin);
|
const message = hasBenefitsFromUniversity
|
||||||
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?"
|
? "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?";
|
: "Are you sure you want to update your e-mail address?";
|
||||||
|
|
||||||
@@ -212,7 +274,9 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
|
|
||||||
const ExpirationDate = () => (
|
const ExpirationDate = () => (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Expiry Date (click to purchase)</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Expiry Date (click to purchase)
|
||||||
|
</label>
|
||||||
<Link
|
<Link
|
||||||
href="/payment"
|
href="/payment"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -221,33 +285,45 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
!user.subscriptionExpirationDate
|
!user.subscriptionExpirationDate
|
||||||
? "!bg-mti-green-ultralight !border-mti-green-light"
|
? "!bg-mti-green-ultralight !border-mti-green-light"
|
||||||
: expirationDateColor(user.subscriptionExpirationDate),
|
: expirationDateColor(user.subscriptionExpirationDate),
|
||||||
"bg-white border-mti-gray-platinum",
|
"bg-white border-mti-gray-platinum"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{!user.subscriptionExpirationDate && "Unlimited"}
|
{!user.subscriptionExpirationDate && "Unlimited"}
|
||||||
{user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
{user.subscriptionExpirationDate &&
|
||||||
|
moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const TimezoneInput = () => (
|
const TimezoneInput = () => (
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timezone</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Timezone
|
||||||
|
</label>
|
||||||
<TimezoneSelect value={timezone} onChange={setTimezone} />
|
<TimezoneSelect value={timezone} onChange={setTimezone} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Layout user={user}>
|
<>
|
||||||
<section className="w-full flex flex-col gap-4 md:gap-8 px-4 py-8">
|
<section className="w-full flex flex-col gap-4 md:gap-8 px-4 py-8">
|
||||||
<h1 className="text-4xl font-bold mb-6 md:hidden">Edit Profile</h1>
|
<h1 className="text-4xl font-bold mb-6 md:hidden">Edit Profile</h1>
|
||||||
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
<div className="flex -md:flex-col-reverse -md:items-center w-full justify-between">
|
||||||
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
<div className="flex flex-col gap-8 w-full md:w-2/3">
|
||||||
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
<h1 className="text-4xl font-bold mb-6 -md:hidden">Edit Profile</h1>
|
||||||
<form className="flex flex-col items-center gap-6 w-full" onSubmit={(e) => e.preventDefault()}>
|
<form
|
||||||
|
className="flex flex-col items-center gap-6 w-full"
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
{user.type !== "corporate" && user.type !== "mastercorporate" && (
|
{user.type !== "corporate" &&
|
||||||
|
user.type !== "mastercorporate" && (
|
||||||
<Input
|
<Input
|
||||||
label={user.type === "agent" ? "English name" : "Name"}
|
label={user.type === "agent" ? "English name" : "Name"}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -323,7 +399,9 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
|
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Country *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Country *
|
||||||
|
</label>
|
||||||
<CountrySelect value={country} onChange={setCountry} />
|
<CountrySelect value={country} onChange={setCountry} />
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
@@ -356,26 +434,37 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{desiredLevels && ["developer", "student"].includes(user.type) && (
|
{desiredLevels &&
|
||||||
|
["developer", "student"].includes(user.type) && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Desired Levels</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Desired Levels
|
||||||
|
</label>
|
||||||
<ModuleLevelSelector
|
<ModuleLevelSelector
|
||||||
levels={desiredLevels}
|
levels={desiredLevels}
|
||||||
setLevels={setDesiredLevels as Dispatch<SetStateAction<{ [key in Module]: number }>>}
|
setLevels={
|
||||||
|
setDesiredLevels as Dispatch<
|
||||||
|
SetStateAction<{ [key in Module]: number }>
|
||||||
|
>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Focus</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Focus
|
||||||
|
</label>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-16 w-full">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFocus("academic")}
|
onClick={() => setFocus("academic")}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
||||||
"hover:bg-mti-purple-light hover:text-white",
|
"hover:bg-mti-purple-light hover:text-white",
|
||||||
focus === "academic" && "!bg-mti-purple-light !text-white",
|
focus === "academic" &&
|
||||||
"transition duration-300 ease-in-out",
|
"!bg-mti-purple-light !text-white",
|
||||||
)}>
|
"transition duration-300 ease-in-out"
|
||||||
|
)}
|
||||||
|
>
|
||||||
Academic
|
Academic
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -383,9 +472,11 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
"w-full border border-mti-gray-platinum rounded-full px-6 py-4 flex justify-center items-center gap-12 bg-white",
|
||||||
"hover:bg-mti-purple-light hover:text-white",
|
"hover:bg-mti-purple-light hover:text-white",
|
||||||
focus === "general" && "!bg-mti-purple-light !text-white",
|
focus === "general" &&
|
||||||
"transition duration-300 ease-in-out",
|
"!bg-mti-purple-light !text-white",
|
||||||
)}>
|
"transition duration-300 ease-in-out"
|
||||||
|
)}
|
||||||
|
>
|
||||||
General
|
General
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -393,18 +484,27 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{preferredGender && ["developer", "student"].includes(user.type) && (
|
{preferredGender &&
|
||||||
|
["developer", "student"].includes(user.type) && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Speaking Instructor's Gender
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={{
|
value={{
|
||||||
value: preferredGender,
|
value: preferredGender,
|
||||||
label: capitalize(preferredGender),
|
label: capitalize(preferredGender),
|
||||||
}}
|
}}
|
||||||
onChange={(value) => (value ? setPreferredGender(value.value as InstructorGender) : null)}
|
onChange={(value) =>
|
||||||
|
value
|
||||||
|
? setPreferredGender(
|
||||||
|
value.value as InstructorGender
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{ value: "male", label: "Male" },
|
{ value: "male", label: "Male" },
|
||||||
{ value: "female", label: "Female" },
|
{ value: "female", label: "Female" },
|
||||||
@@ -417,12 +517,18 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
Preferred Topics{" "}
|
Preferred Topics{" "}
|
||||||
<span
|
<span
|
||||||
className="tooltip"
|
className="tooltip"
|
||||||
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams.">
|
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams."
|
||||||
|
>
|
||||||
<BsQuestionCircleFill />
|
<BsQuestionCircleFill />
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Button className="w-full" variant="outline" onClick={() => setIsPreferredTopicsOpen(true)}>
|
<Button
|
||||||
Select Topics ({preferredTopics?.length || "All"} selected)
|
className="w-full"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsPreferredTopicsOpen(true)}
|
||||||
|
>
|
||||||
|
Select Topics ({preferredTopics?.length || "All"}{" "}
|
||||||
|
selected)
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
@@ -483,14 +589,16 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user.type === "corporate" && user.corporateInformation.referralAgent && (
|
{user.type === "corporate" &&
|
||||||
|
user.corporateInformation.referralAgent &&
|
||||||
|
referralAgent && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<Input
|
<Input
|
||||||
name="agentName"
|
name="agentName"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name}
|
defaultValue={referralAgent?.name}
|
||||||
type="text"
|
type="text"
|
||||||
label="Country Manager's Name"
|
label="Country Manager's Name"
|
||||||
placeholder="Not available"
|
placeholder="Not available"
|
||||||
@@ -500,7 +608,7 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
<Input
|
<Input
|
||||||
name="agentEmail"
|
name="agentEmail"
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email}
|
defaultValue={referralAgent?.email}
|
||||||
type="text"
|
type="text"
|
||||||
label="Country Manager's E-mail"
|
label="Country Manager's E-mail"
|
||||||
placeholder="Not available"
|
placeholder="Not available"
|
||||||
@@ -510,12 +618,11 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
</DoubleColumnRow>
|
</DoubleColumnRow>
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Country Manager's Country *</label>
|
<label className="font-normal text-base text-mti-gray-dim">
|
||||||
|
Country Manager's Country *
|
||||||
|
</label>
|
||||||
<CountrySelect
|
<CountrySelect
|
||||||
value={
|
value={referralAgent?.demographicInformation?.country}
|
||||||
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation
|
|
||||||
?.country
|
|
||||||
}
|
|
||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
@@ -528,7 +635,7 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
onChange={() => null}
|
onChange={() => null}
|
||||||
placeholder="Not available"
|
placeholder="Not available"
|
||||||
defaultValue={
|
defaultValue={
|
||||||
users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone
|
referralAgent?.demographicInformation?.phone
|
||||||
}
|
}
|
||||||
disabled
|
disabled
|
||||||
required
|
required
|
||||||
@@ -539,7 +646,10 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
|
|
||||||
{user.type !== "corporate" && (
|
{user.type !== "corporate" && (
|
||||||
<DoubleColumnRow>
|
<DoubleColumnRow>
|
||||||
<EmploymentStatusInput value={employment} onChange={setEmployment} />
|
<EmploymentStatusInput
|
||||||
|
value={employment}
|
||||||
|
onChange={setEmployment}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex flex-col gap-8 w-full">
|
||||||
<GenderInput value={gender} onChange={setGender} />
|
<GenderInput value={gender} onChange={setGender} />
|
||||||
@@ -552,37 +662,62 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
<div className="flex flex-col gap-6 w-48">
|
<div className="flex flex-col gap-6 w-48">
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
|
className="flex flex-col gap-3 items-center h-fit cursor-pointer group"
|
||||||
onClick={() => (profilePictureInput.current as any)?.click()}>
|
onClick={() => (profilePictureInput.current as any)?.click()}
|
||||||
|
>
|
||||||
<div className="relative overflow-hidden h-48 w-48 rounded-full">
|
<div className="relative overflow-hidden h-48 w-48 rounded-full">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
"absolute top-0 left-0 bg-mti-purple-light/60 w-full h-full z-20 flex items-center justify-center opacity-0 group-hover:opacity-100",
|
||||||
"transition ease-in-out duration-300",
|
"transition ease-in-out duration-300"
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
|
<BsCamera className="text-6xl text-mti-purple-ultralight/80" />
|
||||||
</div>
|
</div>
|
||||||
<img src={profilePicture} alt={user.name} className="aspect-square drop-shadow-xl self-end object-cover" />
|
<img
|
||||||
|
src={profilePicture}
|
||||||
|
alt={user.name}
|
||||||
|
className="aspect-square drop-shadow-xl self-end object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" className="hidden" onChange={uploadProfilePicture} accept="image/*" ref={profilePictureInput} />
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={uploadProfilePicture}
|
||||||
|
accept="image/*"
|
||||||
|
ref={profilePictureInput}
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
onClick={() => (profilePictureInput.current as any)?.click()}
|
onClick={() => (profilePictureInput.current as any)?.click()}
|
||||||
className="cursor-pointer text-mti-purple-light text-sm">
|
className="cursor-pointer text-mti-purple-light text-sm"
|
||||||
|
>
|
||||||
Change picture
|
Change picture
|
||||||
</span>
|
</span>
|
||||||
<h6 className="font-normal text-base text-mti-gray-taupe">{USER_TYPE_LABELS[user.type]}</h6>
|
<h6 className="font-normal text-base text-mti-gray-taupe">
|
||||||
|
{USER_TYPE_LABELS[user.type]}
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
{user.type === "agent" && (
|
{user.type === "agent" && (
|
||||||
<div className="flag items-center h-fit">
|
<div className="flag items-center h-fit">
|
||||||
<img
|
<img
|
||||||
alt={user.demographicInformation?.country.toLowerCase() + "_flag"}
|
alt={
|
||||||
|
user.demographicInformation?.country.toLowerCase() + "_flag"
|
||||||
|
}
|
||||||
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
|
src={`https://flagcdn.com/w320/${user.demographicInformation?.country.toLowerCase()}.png`}
|
||||||
width="320"
|
width="320"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{manualDownloadLink && (
|
{manualDownloadLink && (
|
||||||
<a href={manualDownloadLink} className="max-w-[200px] self-end w-full" download>
|
<a
|
||||||
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
|
href={manualDownloadLink}
|
||||||
|
className="max-w-[200px] self-end w-full"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
className="max-w-[200px] self-end w-full"
|
||||||
|
>
|
||||||
Download Manual
|
Download Manual
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
@@ -601,20 +736,33 @@ function UserProfile({ user, mutateUser, linkedCorporate, groups, users }: Props
|
|||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||||
<Link href="/" className="max-w-[200px] self-end w-full">
|
<Link href="/" className="max-w-[200px] self-end w-full">
|
||||||
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full">
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
className="max-w-[200px] self-end w-full"
|
||||||
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button color="purple" className="max-w-[200px] self-end w-full" onClick={updateUser} disabled={isLoading}>
|
<Button
|
||||||
|
color="purple"
|
||||||
|
className="max-w-[200px] self-end w-full"
|
||||||
|
onClick={updateUser}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home(props: { linkedCorporate?: CorporateUser | MasterCorporateUser; groups: Group[]; users: User[] }) {
|
export default function Home(props: {
|
||||||
|
hasBenefitsFromUniversity: boolean;
|
||||||
|
referralAgent?: User;
|
||||||
|
linkedCorporate?: CorporateUser | MasterCorporateUser;
|
||||||
|
}) {
|
||||||
const { user, mutateUser } = useUser({ redirectTo: "/login" });
|
const { user, mutateUser } = useUser({ redirectTo: "/login" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
|||||||
import { groupByDate } from "@/utils/stats";
|
import { groupByDate } from "@/utils/stats";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import { uuidv4 } from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
@@ -193,7 +192,7 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user}>
|
<>
|
||||||
<RecordFilter user={user} isAdmin={isAdmin} entities={entities} filterState={{ filter: filter, setFilter: setFilter }}>
|
<RecordFilter user={user} isAdmin={isAdmin} entities={entities} filterState={{ filter: filter, setFilter: setFilter }}>
|
||||||
{training && (
|
{training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -226,7 +225,7 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import clsx from "clsx";
|
|||||||
import {Tab} from "@headlessui/react";
|
import {Tab} from "@headlessui/react";
|
||||||
import RegisterIndividual from "./(register)/RegisterIndividual";
|
import RegisterIndividual from "./(register)/RegisterIndividual";
|
||||||
import RegisterCorporate from "./(register)/RegisterCorporate";
|
import RegisterCorporate from "./(register)/RegisterCorporate";
|
||||||
import EmailVerification from "./(auth)/EmailVerification";
|
|
||||||
import {sendEmailVerification} from "@/utils/email";
|
import {sendEmailVerification} from "@/utils/email";
|
||||||
import useUsers from "@/hooks/useUsers";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const getServerSideProps = (context: any) => {
|
export const getServerSideProps = (context: any) => {
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import Head from "next/head";
|
|||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import CodeGenerator from "./(admin)/CodeGenerator";
|
import CodeGenerator from "./(admin)/CodeGenerator";
|
||||||
import ExamLoader from "./(admin)/ExamLoader";
|
import ExamLoader from "./(admin)/ExamLoader";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import Lists from "./(admin)/Lists";
|
import Lists from "./(admin)/Lists";
|
||||||
import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
|
import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import BatchCreateUser from "./(admin)/Lists/BatchCreateUser";
|
import BatchCreateUser from "./(admin)/Lists/BatchCreateUser";
|
||||||
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import IconCard from "@/components/IconCard";
|
import IconCard from "@/components/IconCard";
|
||||||
@@ -23,10 +19,9 @@ import CorporateGradingSystem from "./(admin)/CorporateGradingSystem";
|
|||||||
import { CEFR_STEPS } from "@/resources/grading";
|
import { CEFR_STEPS } from "@/resources/grading";
|
||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { getUserPermissions } from "@/utils/permissions.be";
|
import { getUserPermissions } from "@/utils/permissions.be";
|
||||||
import { Permission, PermissionType } from "@/interfaces/permissions";
|
import { PermissionType } from "@/interfaces/permissions";
|
||||||
import { getUsers } from "@/utils/users.be";
|
import { getUsers } from "@/utils/users.be";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getEntitiesWithRoles, getEntityWithRoles } from "@/utils/entities.be";
|
|
||||||
import { mapBy, serialize, redirect } from "@/utils";
|
import { mapBy, serialize, redirect } from "@/utils";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
@@ -84,7 +79,7 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Layout user={user} className="gap-6">
|
<>
|
||||||
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)} maxWidth="max-w-[85%]">
|
<Modal isOpen={modalOpen === "batchCreateUser"} onClose={() => setModalOpen(undefined)} maxWidth="max-w-[85%]">
|
||||||
<BatchCreateUser
|
<BatchCreateUser
|
||||||
user={user}
|
user={user}
|
||||||
@@ -180,7 +175,7 @@ export default function Admin({ user, entities, permissions, allUsers, entitiesG
|
|||||||
<section className="w-full">
|
<section className="w-full">
|
||||||
<Lists user={user} entities={entities} permissions={permissions} />
|
<Lists user={user} entities={entities} permissions={permissions} />
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import Table from "@/components/High/Table";
|
import Table from "@/components/High/Table";
|
||||||
import Checkbox from "@/components/Low/Checkbox";
|
import Checkbox from "@/components/Low/Checkbox";
|
||||||
import Separator from "@/components/Low/Separator";
|
import Separator from "@/components/Low/Separator";
|
||||||
@@ -8,16 +7,15 @@ import { Session } from "@/hooks/useSessions";
|
|||||||
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
import { Entity, EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Exam } from "@/interfaces/exam";
|
import { Exam } from "@/interfaces/exam";
|
||||||
import { Assignment, AssignmentResult } from "@/interfaces/results";
|
import { Assignment, AssignmentResult } from "@/interfaces/results";
|
||||||
import { Group, Stat, StudentUser, User } from "@/interfaces/user";
|
import { StudentUser, User } from "@/interfaces/user";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { getEntitiesAssignments } from "@/utils/assignments.be";
|
import { getEntitiesAssignments } from "@/utils/assignments.be";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getExamsByIds } from "@/utils/exams.be";
|
import { getExamsByIds } from "@/utils/exams.be";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getSessionsByAssignments, getSessionsByUser } from "@/utils/sessions.be";
|
import { getSessionsByAssignments } from "@/utils/sessions.be";
|
||||||
import { getStatsByUsers } from "@/utils/stats.be";
|
|
||||||
import { isAdmin } from "@/utils/users";
|
import { isAdmin } from "@/utils/users";
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
@@ -222,7 +220,7 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -301,7 +299,7 @@ export default function Statistical({ user, students, entities, assignments, ses
|
|||||||
isDownloadLoading={isDownloading}
|
isDownloadLoading={isDownloading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ import {
|
|||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useStats from "@/hooks/useStats";
|
||||||
import {
|
import {
|
||||||
averageScore,
|
|
||||||
groupBySession,
|
groupBySession,
|
||||||
groupByModule,
|
groupByModule,
|
||||||
timestampToMoment,
|
timestampToMoment,
|
||||||
@@ -32,11 +31,8 @@ import { ToastContainer } from "react-toastify";
|
|||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import { calculateBandScore } from "@/utils/score";
|
import { calculateBandScore } from "@/utils/score";
|
||||||
import {
|
import {
|
||||||
countExamModules,
|
|
||||||
countFullExams,
|
|
||||||
MODULE_ARRAY,
|
MODULE_ARRAY,
|
||||||
sortByModule,
|
sortByModule,
|
||||||
} from "@/utils/moduleUtils";
|
} from "@/utils/moduleUtils";
|
||||||
@@ -109,7 +105,7 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
const [selectedEntity, setSelectedEntity] = useState<string>();
|
const [selectedEntity, setSelectedEntity] = useState<string>();
|
||||||
|
|
||||||
const entitiesToSearch = useMemo(() => {
|
const entitiesToSearch = useMemo(() => {
|
||||||
if(selectedEntity) return [selectedEntity]
|
if (selectedEntity) return [selectedEntity];
|
||||||
if (isAdmin) return undefined;
|
if (isAdmin) return undefined;
|
||||||
return mapBy(entities, "id");
|
return mapBy(entities, "id");
|
||||||
}, [entities, isAdmin, selectedEntity]);
|
}, [entities, isAdmin, selectedEntity]);
|
||||||
@@ -137,10 +133,19 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
|
|
||||||
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
||||||
|
|
||||||
const { data: stats } = useFilterRecordsByUser<Stat[]>(
|
const {
|
||||||
statsUserId,
|
data: {
|
||||||
!statsUserId
|
allStats: stats = [],
|
||||||
);
|
fullExams: exams = 0,
|
||||||
|
uniqueModules: modules = 0,
|
||||||
|
averageScore = 0,
|
||||||
|
},
|
||||||
|
} = useStats<{
|
||||||
|
allStats: Stat[];
|
||||||
|
fullExams: number;
|
||||||
|
uniqueModules: number;
|
||||||
|
averageScore: number;
|
||||||
|
}>(statsUserId, !statsUserId, "stats");
|
||||||
|
|
||||||
const initialStatDate = useMemo(
|
const initialStatDate = useMemo(
|
||||||
() => (stats[0] ? timestampToMoment(stats[0]).toDate() : null),
|
() => (stats[0] ? timestampToMoment(stats[0]).toDate() : null),
|
||||||
@@ -239,7 +244,7 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user} className="gap-8">
|
<>
|
||||||
<ProfileSummary
|
<ProfileSummary
|
||||||
user={userData || user}
|
user={userData || user}
|
||||||
items={[
|
items={[
|
||||||
@@ -247,7 +252,7 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
icon: (
|
icon: (
|
||||||
<BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
<BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
||||||
),
|
),
|
||||||
value: countFullExams(stats),
|
value: exams,
|
||||||
label: "Exams",
|
label: "Exams",
|
||||||
tooltip: "Number of all conducted completed exams",
|
tooltip: "Number of all conducted completed exams",
|
||||||
},
|
},
|
||||||
@@ -255,7 +260,7 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
icon: (
|
icon: (
|
||||||
<BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
<BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
||||||
),
|
),
|
||||||
value: countExamModules(stats),
|
value: modules,
|
||||||
label: "Modules",
|
label: "Modules",
|
||||||
tooltip:
|
tooltip:
|
||||||
"Number of all exam modules performed including Level Test",
|
"Number of all exam modules performed including Level Test",
|
||||||
@@ -264,7 +269,7 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
icon: (
|
icon: (
|
||||||
<BsStar className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
<BsStar className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />
|
||||||
),
|
),
|
||||||
value: `${stats.length > 0 ? averageScore(stats) : 0}%`,
|
value: `${averageScore.toFixed(2) || 0}%`,
|
||||||
label: "Average Score",
|
label: "Average Score",
|
||||||
tooltip: "Average success rate for questions responded",
|
tooltip: "Average success rate for questions responded",
|
||||||
},
|
},
|
||||||
@@ -888,7 +893,7 @@ export default function Stats({ user, entities, isAdmin }: Props) {
|
|||||||
<span className="font-semibold ml-1">No stats to display...</span>
|
<span className="font-semibold ml-1">No stats to display...</span>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import TicketDisplay from "@/components/High/TicketDisplay";
|
import TicketDisplay from "@/components/High/TicketDisplay";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
@@ -228,7 +227,7 @@ export default function Tickets() {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user} className="gap-6">
|
<>
|
||||||
<h1 className="text-2xl font-semibold">Tickets</h1>
|
<h1 className="text-2xl font-semibold">Tickets</h1>
|
||||||
|
|
||||||
<div className="flex w-full items-center gap-4">
|
<div className="flex w-full items-center gap-4">
|
||||||
@@ -317,7 +316,7 @@ export default function Tickets() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Layout>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { ITrainingContent, ITrainingTip } from "@/training/TrainingInterfaces";
|
|||||||
import formatTip from "@/training/FormatTip";
|
import formatTip from "@/training/FormatTip";
|
||||||
import { Stat, User } from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
@@ -149,7 +148,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<Layout user={user}>
|
<>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
@@ -343,7 +342,7 @@ const TrainingContent: React.FC<{ user: User }> = ({ user }) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {withIronSessionSsr} from "iron-session/next";
|
|||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {User} from "@/interfaces/user";
|
import {User} from "@/interfaces/user";
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@@ -22,10 +21,8 @@ import RecordFilter from "@/components/Medium/RecordFilter";
|
|||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
|
|
||||||
import { getEntitiesUsers } from "@/utils/users.be";
|
import { getEntitiesUsers } from "@/utils/users.be";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import { Assignment } from "@/interfaces/results";
|
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
||||||
@@ -179,7 +176,7 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<Layout user={user}>
|
<>
|
||||||
{isNewContentLoading || areRecordsLoading ? (
|
{isNewContentLoading || areRecordsLoading ? (
|
||||||
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
<div className="absolute left-1/2 top-1/2 flex h-fit w-fit -translate-x-1/2 -translate-y-1/2 animate-pulse flex-col items-center gap-12">
|
||||||
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
<span className="loading loading-infinity w-32 bg-mti-green-light" />
|
||||||
@@ -220,7 +217,7 @@ const Training: React.FC<{user: User, entities: EntityWithRoles[], users: User[]
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import useUser from "@/hooks/useUser";
|
|
||||||
import useUsers from "@/hooks/useUsers";
|
|
||||||
import { Type, User } from "@/interfaces/user";
|
import { Type, User } from "@/interfaces/user";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import useFilterStore from "@/stores/listFilterStore";
|
import useFilterStore from "@/stores/listFilterStore";
|
||||||
@@ -10,8 +8,7 @@ import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
|||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import {useEffect} from "react";
|
import { BsChevronLeft} from "react-icons/bs";
|
||||||
import {BsArrowLeft, BsChevronLeft} from "react-icons/bs";
|
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import UserList from "../(admin)/Lists/UserList";
|
import UserList from "../(admin)/Lists/UserList";
|
||||||
|
|
||||||
@@ -51,7 +48,7 @@ export default function UsersListPage({ user, type }: Props) {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<Layout user={user}>
|
<>
|
||||||
<UserList
|
<UserList
|
||||||
user={user}
|
user={user}
|
||||||
type={type}
|
type={type}
|
||||||
@@ -70,7 +67,7 @@ export default function UsersListPage({ user, type }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useGroups from "@/hooks/useGroups";
|
|
||||||
import useUsers, { userHashStudent } from "@/hooks/useUsers";
|
|
||||||
import { Group, Stat, StudentUser, User } from "@/interfaces/user";
|
import { Group, Stat, StudentUser, User } from "@/interfaces/user";
|
||||||
import { getUserCompanyName } from "@/resources/user";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BsArrowLeft, BsArrowRepeat, BsChevronLeft } from "react-icons/bs";
|
import { BsChevronLeft } from "react-icons/bs";
|
||||||
import { mapBy, serialize } from "@/utils";
|
import { mapBy, serialize } from "@/utils";
|
||||||
import { withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
|
||||||
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { Entity } from "@/interfaces/entity";
|
import { Entity } from "@/interfaces/entity";
|
||||||
import { getParticipantGroups, getParticipantsGroups } from "@/utils/groups.be";
|
import { getParticipantsGroups } from "@/utils/groups.be";
|
||||||
import StudentPerformanceList from "../(admin)/Lists/StudentPerformanceList";
|
import StudentPerformanceList from "../(admin)/Lists/StudentPerformanceList";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import Layout from "@/components/High/Layout";
|
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { redirect } from "@/utils";
|
import { redirect } from "@/utils";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
@@ -74,7 +69,7 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
|||||||
</Head>
|
</Head>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<Layout user={user}>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -86,7 +81,7 @@ const StudentPerformance = ({ user, students, entities, groups }: Props) => {
|
|||||||
<h2 className="font-bold text-2xl">Student Performance ({students.length})</h2>
|
<h2 className="font-bold text-2xl">Student Performance ({students.length})</h2>
|
||||||
</div>
|
</div>
|
||||||
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
<StudentPerformanceList items={performanceStudents} stats={stats} />
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { IncomingMessage, ServerResponse } from "http";
|
import { IncomingMessage, ServerResponse } from "http";
|
||||||
import { IronSession } from "iron-session";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getUser } from "./users.be";
|
import { getUser } from "./users.be";
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,72 @@ export const getAssignment = async (id: string) => {
|
|||||||
return await db.collection("assignments").findOne<Assignment>({ id });
|
return await db.collection("assignments").findOne<Assignment>({ id });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssignmentsByAssignee = async (id: string, filter?: { [key in keyof Partial<Assignment>]: any }) => {
|
export const getAssignmentsByAssignee = async (id: string, filter?: {}, projection?: {}, sort?: {}) => {
|
||||||
return await db
|
return await db.collection("assignments")
|
||||||
.collection("assignments")
|
.aggregate([
|
||||||
.find<Assignment>({ assignees: id, ...(!filter ? {} : filter) })
|
{ $match: { assignees: id, ...(!filter ? {} : filter) } },
|
||||||
|
...(sort ? [{ $sort: sort }] : []),
|
||||||
|
...(projection ? [{ $project: projection }] : []),
|
||||||
|
])
|
||||||
.toArray();
|
.toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAssignmentsForStudent = async (id: string, currentDate: string) => {
|
||||||
|
return await db.collection("assignments")
|
||||||
|
.aggregate([{
|
||||||
|
$match: {
|
||||||
|
assignees: id, archived: { $ne: true },
|
||||||
|
endDate: { $gte: currentDate },
|
||||||
|
$or: [
|
||||||
|
{ autoStart: true, startDate: { $lt: currentDate } },
|
||||||
|
{ start: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ $sort: { startDate: 1 } },
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
startDate: 1,
|
||||||
|
endDate: 1,
|
||||||
|
exams: {
|
||||||
|
$sortArray: {
|
||||||
|
input: {
|
||||||
|
$filter: {
|
||||||
|
input: "$exams",
|
||||||
|
as: "exam",
|
||||||
|
cond: { $eq: ["$$exam.assignee", id] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sortBy: { module: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasResults: {
|
||||||
|
$cond: {
|
||||||
|
if: {
|
||||||
|
$gt: [
|
||||||
|
{
|
||||||
|
$size: {
|
||||||
|
$filter: {
|
||||||
|
input: "$results",
|
||||||
|
as: "result",
|
||||||
|
cond: { $eq: ["$$result.userId", id] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
then: true,
|
||||||
|
else: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]).toArray()
|
||||||
|
};
|
||||||
|
|
||||||
export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => {
|
export const getAssignmentsByAssignerBetweenDates = async (id: string, startDate: Date, endDate: Date) => {
|
||||||
return await db.collection("assignments").find<Assignment>({ assigner: id }).toArray();
|
return await db.collection("assignments").find<Assignment>({ assigner: id }).toArray();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const getEntitiesWithRoles = async (ids?: string[]): Promise<EntityWithRo
|
|||||||
export const getEntities = async (ids?: string[], projection = {}) => {
|
export const getEntities = async (ids?: string[], projection = {}) => {
|
||||||
return await db
|
return await db
|
||||||
.collection("entities")
|
.collection("entities")
|
||||||
.find<Entity>(ids ? { id: { $in: ids } } : {}, projection)
|
.find<Entity>(ids ? { id: { $in: ids } } : {}, { projection })
|
||||||
.toArray();
|
.toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { CEFR_STEPS } from "@/resources/grading";
|
import { CEFR_STEPS } from "@/resources/grading";
|
||||||
import { getUserCorporate } from "@/utils/groups.be";
|
|
||||||
import { User } from "@/interfaces/user";
|
|
||||||
import { Grading } from "@/interfaces";
|
import { Grading } from "@/interfaces";
|
||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
export const getGradingSystemByEntity = async (id: string) =>
|
export const getGradingSystemByEntity = async (id: string, projection?: {}) =>
|
||||||
(await db.collection("grading").findOne<Grading>({ entity: id })) || { steps: CEFR_STEPS, entity: "" };
|
(await db.collection("grading").findOne<Grading>({ entity: id }, {
|
||||||
|
projection: projection,
|
||||||
|
})) || { steps: CEFR_STEPS, entity: "" };
|
||||||
|
|
||||||
export const getGradingSystemByEntities = async (ids: string[]) =>
|
export const getGradingSystemByEntities = async (ids: string[]) =>
|
||||||
await db.collection("grading").find<Grading>({ entity: { $in: ids } }).toArray();
|
await db.collection("grading").find<Grading>({ entity: { $in: ids } }).toArray();
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export const getGroups = async (): Promise<WithEntity<Group>[]> => {
|
|||||||
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
|
.aggregate<WithEntity<Group>>(addEntityToGroupPipeline).toArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParticipantGroups = async (id: string) => {
|
export const getParticipantGroups = async (id: string, projection?: {}) => {
|
||||||
return await db.collection("groups").find<Group>({ participants: id }).toArray();
|
return await db.collection("groups").find<Group>({ participants: id }, { projection: projection }).toArray();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParticipantsGroups = async (ids: string[]) => {
|
export const getParticipantsGroups = async (ids: string[]) => {
|
||||||
|
|||||||
@@ -36,6 +36,43 @@ export function checkAccess(user: User, types: Type[], permissions?: PermissionT
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupAllowedEntitiesByPermissions(
|
||||||
|
user: User,
|
||||||
|
entities: EntityWithRoles[],
|
||||||
|
permissions: RolePermission[]
|
||||||
|
): { [key: string]: EntityWithRoles[] } {
|
||||||
|
if (["admin", "developer"].includes(user?.type)) {
|
||||||
|
return permissions.reduce((acc, permission) => {
|
||||||
|
acc[permission] = entities;
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: EntityWithRoles[] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userEntityMap = new Map(user.entities.map(e => [e.id, e]));
|
||||||
|
const roleCache = new Map<string, Role | null>();
|
||||||
|
|
||||||
|
return entities.reduce((acc, entity) => {
|
||||||
|
const userEntity = userEntityMap.get(entity.id);
|
||||||
|
const role = userEntity
|
||||||
|
? roleCache.get(userEntity.role) ??
|
||||||
|
(() => {
|
||||||
|
const foundRole = entity.roles.find(r => r.id === userEntity.role) || null;
|
||||||
|
roleCache.set(userEntity.role, foundRole);
|
||||||
|
return foundRole;
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
permissions.forEach(permission => {
|
||||||
|
if (!acc[permission]) acc[permission] = [];
|
||||||
|
if (role && role.permissions.includes(permission)) {
|
||||||
|
acc[permission].push(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: EntityWithRoles[] });
|
||||||
|
}
|
||||||
|
|
||||||
export function findAllowedEntities(user: User, entities: EntityWithRoles[], permission: RolePermission) {
|
export function findAllowedEntities(user: User, entities: EntityWithRoles[], permission: RolePermission) {
|
||||||
if (["admin", "developer"].includes(user?.type)) return entities
|
if (["admin", "developer"].includes(user?.type)) return entities
|
||||||
|
|
||||||
@@ -52,7 +89,6 @@ export function findAllowedEntitiesSomePermissions(user: User, entities: EntityW
|
|||||||
|
|
||||||
export function doesEntityAllow(user: User, entity: EntityWithRoles, permission: RolePermission) {
|
export function doesEntityAllow(user: User, entity: EntityWithRoles, permission: RolePermission) {
|
||||||
if (isAdmin(user)) return true
|
if (isAdmin(user)) return true
|
||||||
|
|
||||||
const userEntity = findBy(user.entities, 'id', entity?.id)
|
const userEntity = findBy(user.entities, 'id', entity?.id)
|
||||||
if (!userEntity) return false
|
if (!userEntity) return false
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,131 @@ export const getStatsByUsers = async (ids: string[]) =>
|
|||||||
.collection("stats")
|
.collection("stats")
|
||||||
.find<Stat>({ user: { $in: ids } })
|
.find<Stat>({ user: { $in: ids } })
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
|
export const getDetailedStatsByUser = async (id: string, query?: string) => {
|
||||||
|
let aggregateArray: any[] = [
|
||||||
|
{ $match: { user: id } },
|
||||||
|
{ $sort: { "date": 1 } },
|
||||||
|
]
|
||||||
|
switch (query) {
|
||||||
|
case "stats":
|
||||||
|
{
|
||||||
|
aggregateArray = aggregateArray.concat([{
|
||||||
|
$group: {
|
||||||
|
_id: "$session",
|
||||||
|
modules: { $addToSet: "$module" },
|
||||||
|
documents: { $push: "$$ROOT" },
|
||||||
|
totalCorrect: { $sum: "$score.correct" },
|
||||||
|
totalQuestions: { $sum: "$score.total" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
hasAllModules: {
|
||||||
|
$eq: [
|
||||||
|
{ $size: { $setIntersection: ["$modules", ["reading", "listening", "writing", "speaking"]] } },
|
||||||
|
4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
uniqueModulesCount: { $size: "$modules" },
|
||||||
|
averageScore: {
|
||||||
|
$cond: [
|
||||||
|
{ $gt: ["$totalQuestions", 0] },
|
||||||
|
{ $multiply: [{ $divide: ["$totalCorrect", "$totalQuestions"] }, 100] },
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
documents: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
fullExams: { $sum: { $cond: ["$hasAllModules", 1, 0] } },
|
||||||
|
uniqueModules: { $sum: "$uniqueModulesCount" },
|
||||||
|
averageScore: {
|
||||||
|
$avg: "$averageScore"
|
||||||
|
},
|
||||||
|
allStats: { $push: "$documents" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
fullExams: 1,
|
||||||
|
uniqueModules: 1,
|
||||||
|
averageScore: 1,
|
||||||
|
allStats: {
|
||||||
|
$reduce: {
|
||||||
|
input: "$allStats",
|
||||||
|
initialValue: [],
|
||||||
|
in: { $concatArrays: ["$$value", "$$this"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "byModule": {
|
||||||
|
aggregateArray = aggregateArray.concat([{
|
||||||
|
$facet: {
|
||||||
|
moduleCounts: [
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
module: "$module",
|
||||||
|
session: "$session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$_id.module",
|
||||||
|
count: {
|
||||||
|
$count: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
module: "$_id",
|
||||||
|
count: "$count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
allDocuments: [
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
moduleCount: {
|
||||||
|
$arrayToObject: {
|
||||||
|
$map: {
|
||||||
|
input: "$moduleCounts",
|
||||||
|
as: "module",
|
||||||
|
in: {
|
||||||
|
k: "$$module.module",
|
||||||
|
v: "$$module.count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allDocs: "$allDocuments"
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return await db.collection("stats").aggregate(aggregateArray).toArray().then((result) => query ? result[0] : result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import client from "@/lib/mongodb";
|
|||||||
import { EntityWithRoles, WithEntities } from "@/interfaces/entity";
|
import { EntityWithRoles, WithEntities } from "@/interfaces/entity";
|
||||||
import { getEntity } from "./entities.be";
|
import { getEntity } from "./entities.be";
|
||||||
import { getRole } from "./roles.be";
|
import { getRole } from "./roles.be";
|
||||||
import { findAllowedEntities } from "./permissions";
|
import { findAllowedEntities, groupAllowedEntitiesByPermissions } from "./permissions";
|
||||||
import { mapBy } from ".";
|
import { mapBy } from ".";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
@@ -75,6 +75,39 @@ export async function countUsers(filter?: object) {
|
|||||||
.countDocuments(filter || {})
|
.countDocuments(filter || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function countUsersByTypes(types: Type[]) {
|
||||||
|
return await db
|
||||||
|
.collection("users")
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
type: { $in: types } // Filter only specified types
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$type",
|
||||||
|
count: { $sum: 1 } // Count documents in each group
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
counts: {
|
||||||
|
$push: { k: "$_id", v: "$count" } // Convert to key-value pairs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
result: { $arrayToObject: "$counts" } // Convert key-value pairs to an object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]).toArray().then(([{ result }]) => result);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserWithEntity(id: string): Promise<WithEntities<User> | undefined> {
|
export async function getUserWithEntity(id: string): Promise<WithEntities<User> | undefined> {
|
||||||
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
||||||
if (!user) return undefined;
|
if (!user) return undefined;
|
||||||
@@ -91,8 +124,8 @@ export async function getUserWithEntity(id: string): Promise<WithEntities<User>
|
|||||||
return { ...user, entities };
|
return { ...user, entities };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUser(id: string): Promise<User | undefined> {
|
export async function getUser(id: string, projection = {}): Promise<User | undefined> {
|
||||||
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0 } });
|
const user = await db.collection("users").findOne<User>({ id: id }, { projection: { _id: 0, ...projection } });
|
||||||
return !!user ? user : undefined;
|
return !!user ? user : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +153,7 @@ export async function countEntityUsers(id: string, filter?: object) {
|
|||||||
export async function getEntitiesUsers(ids: string[], filter?: object, limit?: number, projection = {}) {
|
export async function getEntitiesUsers(ids: string[], filter?: object, limit?: number, projection = {}) {
|
||||||
return await db
|
return await db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) }, projection)
|
.find<User>({ "entities.id": { $in: ids }, ...(filter || {}) }, { projection })
|
||||||
.limit(limit || 0)
|
.limit(limit || 0)
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
@@ -199,29 +232,45 @@ export async function getUserBalance(user: User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
|
export const filterAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
|
||||||
const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students')
|
|
||||||
const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers')
|
|
||||||
const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates')
|
|
||||||
const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates')
|
|
||||||
|
|
||||||
const students = await getEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" })
|
const {
|
||||||
const teachers = await getEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), { type: "teacher" })
|
["view_students"]: allowedStudentEntities,
|
||||||
const corporates = await getEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), { type: "corporate" })
|
["view_teachers"]: allowedTeacherEntities,
|
||||||
const masterCorporates = await getEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), { type: "mastercorporate" })
|
["view_corporates"]: allowedCorporateEntities,
|
||||||
|
["view_mastercorporates"]: allowedMasterCorporateEntities,
|
||||||
|
} = groupAllowedEntitiesByPermissions(user, entities, [
|
||||||
|
"view_students",
|
||||||
|
"view_teachers",
|
||||||
|
'view_corporates',
|
||||||
|
'view_mastercorporates',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const students = await getEntitiesUsers(mapBy(allowedStudentEntities, 'id'), { type: "student" })
|
||||||
|
const teachers = await getEntitiesUsers(mapBy(allowedTeacherEntities, 'id'), { type: "teacher" })
|
||||||
|
const corporates = await getEntitiesUsers(mapBy(allowedCorporateEntities, 'id'), { type: "corporate" })
|
||||||
|
const masterCorporates = await getEntitiesUsers(mapBy(allowedMasterCorporateEntities, 'id'), { type: "mastercorporate" })
|
||||||
|
|
||||||
return [...students, ...teachers, ...corporates, ...masterCorporates]
|
return [...students, ...teachers, ...corporates, ...masterCorporates]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const countAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
|
export const countAllowedUsers = async (user: User, entities: EntityWithRoles[]) => {
|
||||||
const studentsAllowedEntities = findAllowedEntities(user, entities, 'view_students')
|
const {
|
||||||
const teachersAllowedEntities = findAllowedEntities(user, entities, 'view_teachers')
|
["view_students"]: allowedStudentEntities,
|
||||||
const corporateAllowedEntities = findAllowedEntities(user, entities, 'view_corporates')
|
["view_teachers"]: allowedTeacherEntities,
|
||||||
const masterCorporateAllowedEntities = findAllowedEntities(user, entities, 'view_mastercorporates')
|
["view_corporates"]: allowedCorporateEntities,
|
||||||
|
["view_mastercorporates"]: allowedMasterCorporateEntities,
|
||||||
|
} = groupAllowedEntitiesByPermissions(user, entities, [
|
||||||
|
"view_students",
|
||||||
|
"view_teachers",
|
||||||
|
'view_corporates',
|
||||||
|
'view_mastercorporates',
|
||||||
|
]);
|
||||||
|
|
||||||
const student = await countEntitiesUsers(mapBy(studentsAllowedEntities, 'id'), { type: "student" })
|
const student = await countEntitiesUsers(mapBy(allowedStudentEntities, 'id'), { type: "student" })
|
||||||
const teacher = await countEntitiesUsers(mapBy(teachersAllowedEntities, 'id'), { type: "teacher" })
|
const teacher = await countEntitiesUsers(mapBy(allowedTeacherEntities, 'id'), { type: "teacher" })
|
||||||
const corporate = await countEntitiesUsers(mapBy(corporateAllowedEntities, 'id'), { type: "corporate" })
|
const corporate = await countEntitiesUsers(mapBy(allowedCorporateEntities, 'id'), { type: "corporate" })
|
||||||
const mastercorporate = await countEntitiesUsers(mapBy(masterCorporateAllowedEntities, 'id'), { type: "mastercorporate" })
|
const mastercorporate = await countEntitiesUsers(mapBy(allowedMasterCorporateEntities, 'id'), { type: "mastercorporate" })
|
||||||
|
|
||||||
return { student, teacher, corporate, mastercorporate }
|
return { student, teacher, corporate, mastercorporate }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user