Merge branch 'develop' of bitbucket.org:ecropdev/ielts-ui into develop
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
import { FillBlanksExercise, FillBlanksMCOption } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import { CommonProps } from "..";
|
import { CommonProps } from "..";
|
||||||
import Button from "../../Low/Button";
|
import Button from "../../Low/Button";
|
||||||
@@ -45,7 +45,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
let correctWords: any;
|
let correctWords: any;
|
||||||
if (exam && exam.module === "level" && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") {
|
if (exam && (exam.module === "level" || exam.module === "reading") && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") {
|
||||||
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +55,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
|
||||||
if (!solution) return false;
|
if (!solution) return false;
|
||||||
const option = correctWords!.find((w: any) => {
|
const option = correctWords!.find((w: any) => {
|
||||||
|
console.log(w);
|
||||||
if (typeof w === "string") {
|
if (typeof w === "string") {
|
||||||
return w.toLowerCase() === x.solution.toLowerCase();
|
return w.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else if ('letter' in w) {
|
} else if ('letter' in w) {
|
||||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
return w.id.toString() === x.id.toString();
|
return w.id.toString() === x.id.toString();
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
return { total, correct, missing };
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
const renderLines = (line: string) => {
|
const renderLines = useCallback((line: string) => {
|
||||||
return (
|
return (
|
||||||
<div className="text-base leading-5" key={v4()}>
|
<div className="text-base leading-5" key={v4()}>
|
||||||
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
{reactStringReplace(line, /({{\d+}})/g, (match) => {
|
||||||
@@ -121,21 +122,34 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}, [variant, words, setCurrentMCSelection, answers]);
|
||||||
|
|
||||||
|
const memoizedLines = useMemo(() => {
|
||||||
|
return text.split("\\n").map((line, index) => (
|
||||||
|
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
|
||||||
|
{renderLines(line)}
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
));
|
||||||
|
}, [text, variant, renderLines]);
|
||||||
|
|
||||||
|
|
||||||
const onSelection = (questionID: string, value: string) => {
|
const onSelection = (questionID: string, value: string) => {
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
//if (variant === "mc") {
|
||||||
|
console.log(answers);
|
||||||
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
|
//}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers])
|
}, [answers])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
||||||
{false && <span className="text-sm w-full leading-6">
|
{variant !== "mc" && <span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
{line}
|
{line}
|
||||||
@@ -144,12 +158,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
))}
|
))}
|
||||||
</span>}
|
</span>}
|
||||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||||
{text.split("\\n").map((line, index) => (
|
{memoizedLines}
|
||||||
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
|
|
||||||
{renderLines(line)}
|
|
||||||
<br />
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</span>
|
</span>
|
||||||
{variant === "mc" && typeCheckWordsMC(words) ? (
|
{variant === "mc" && typeCheckWordsMC(words) ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
206
src/components/Medium/RecordFilter.tsx
Normal file
206
src/components/Medium/RecordFilter.tsx
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { User } from "@/interfaces/user";
|
||||||
|
import { checkAccess } from "@/utils/permissions";
|
||||||
|
import Select from "../Low/Select";
|
||||||
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import useUsers from "@/hooks/useUsers";
|
||||||
|
import useGroups from "@/hooks/useGroups";
|
||||||
|
import useRecordStore from "@/stores/recordStore";
|
||||||
|
|
||||||
|
|
||||||
|
type TimeFilter = "months" | "weeks" | "days";
|
||||||
|
type Filter = TimeFilter | "assignments" | undefined;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
filterState: {
|
||||||
|
filter: Filter,
|
||||||
|
setFilter: React.Dispatch<React.SetStateAction<Filter>>
|
||||||
|
},
|
||||||
|
assignments?: boolean;
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSelectableCorporate = {
|
||||||
|
value: "",
|
||||||
|
label: "All",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecordFilter: React.FC<Props> = ({
|
||||||
|
user,
|
||||||
|
filterState,
|
||||||
|
assignments = true,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const { filter, setFilter } = filterState;
|
||||||
|
|
||||||
|
const [statsUserId, setStatsUserId] = useRecordStore((state) => [
|
||||||
|
state.selectedUser,
|
||||||
|
state.setSelectedUser
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { users } = useUsers();
|
||||||
|
const { groups: allGroups } = useGroups({});
|
||||||
|
const { groups } = useGroups({ admin: user?.id, userType: user?.type });
|
||||||
|
|
||||||
|
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
|
||||||
|
setFilter((prev) => (prev === value ? undefined : value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectableCorporates = [
|
||||||
|
defaultSelectableCorporate,
|
||||||
|
...users
|
||||||
|
.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id))
|
||||||
|
.filter((x) => x.type === "corporate")
|
||||||
|
.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
||||||
|
|
||||||
|
const getUsersList = (): User[] => {
|
||||||
|
if (selectedCorporate) {
|
||||||
|
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
|
||||||
|
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
||||||
|
|
||||||
|
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
||||||
|
return userListWithUsers.filter((x) => x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const corporateFilteredUserList = getUsersList();
|
||||||
|
|
||||||
|
const getSelectedUser = () => {
|
||||||
|
if (selectedCorporate) {
|
||||||
|
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
||||||
|
return userInCorporate || corporateFilteredUserList[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return users.find((x) => x.id === statsUserId) || user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedUser = getSelectedUser();
|
||||||
|
const selectedUserSelectValue = selectedUser
|
||||||
|
? {
|
||||||
|
value: selectedUser.id,
|
||||||
|
label: `${selectedUser.name} - ${selectedUser.email}`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
value: "",
|
||||||
|
label: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
||||||
|
<div className="xl:w-3/4">
|
||||||
|
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !children && (
|
||||||
|
<>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
options={selectableCorporates}
|
||||||
|
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
||||||
|
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
||||||
|
styles={{
|
||||||
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
|
option: (styles, state) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
|
}}></Select>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
options={corporateFilteredUserList.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
}))}
|
||||||
|
value={selectedUserSelectValue}
|
||||||
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
|
styles={{
|
||||||
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
|
option: (styles, state) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && !children && (
|
||||||
|
<>
|
||||||
|
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
options={users
|
||||||
|
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
||||||
|
.map((x) => ({
|
||||||
|
value: x.id,
|
||||||
|
label: `${x.name} - ${x.email}`,
|
||||||
|
}))}
|
||||||
|
value={selectedUserSelectValue}
|
||||||
|
onChange={(value) => setStatsUserId(value?.value!)}
|
||||||
|
styles={{
|
||||||
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
|
option: (styles, state) => ({
|
||||||
|
...styles,
|
||||||
|
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
||||||
|
color: state.isFocused ? "black" : styles.color,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
||||||
|
{assignments && (
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
filter === "assignments" && "!bg-mti-purple-light !text-white",
|
||||||
|
)}
|
||||||
|
onClick={() => toggleFilter("assignments")}>
|
||||||
|
Assignments
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
filter === "months" && "!bg-mti-purple-light !text-white",
|
||||||
|
)}
|
||||||
|
onClick={() => toggleFilter("months")}>
|
||||||
|
Last month
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
filter === "weeks" && "!bg-mti-purple-light !text-white",
|
||||||
|
)}
|
||||||
|
onClick={() => toggleFilter("weeks")}>
|
||||||
|
Last week
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
||||||
|
"transition duration-300 ease-in-out",
|
||||||
|
filter === "days" && "!bg-mti-purple-light !text-white",
|
||||||
|
)}
|
||||||
|
onClick={() => toggleFilter("days")}>
|
||||||
|
Last day
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecordFilter;
|
||||||
@@ -4,17 +4,17 @@ import clsx from "clsx";
|
|||||||
import {Stat, User} from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import ai_usage from "@/utils/ai.detection";
|
import ai_usage from "@/utils/ai.detection";
|
||||||
import {calculateBandScore} from "@/utils/score";
|
import { calculateBandScore } from "@/utils/score";
|
||||||
import moment from "moment";
|
import moment from 'moment';
|
||||||
import {Assignment} from "@/interfaces/results";
|
import { Assignment } from '@/interfaces/results';
|
||||||
import {uuidv4} from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import {useRouter} from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {uniqBy} from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
import {sortByModule} from "@/utils/moduleUtils";
|
import { sortByModule } from "@/utils/moduleUtils";
|
||||||
import {convertToUserSolutions} from "@/utils/stats";
|
import { convertToUserSolutions } from "@/utils/stats";
|
||||||
import {getExamById} from "@/utils/exams";
|
import { getExamById } from "@/utils/exams";
|
||||||
import {Exam, UserSolution} from "@/interfaces/exam";
|
import { Exam, UserSolution } from '@/interfaces/exam';
|
||||||
import ModuleBadge from "./ModuleBadge";
|
import ModuleBadge from '../ModuleBadge';
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string | number) => {
|
const formatTimestamp = (timestamp: string | number) => {
|
||||||
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;
|
||||||
@@ -34,7 +34,7 @@ export default function FillBlanksSolutions({
|
|||||||
if (typeof w === "string") {
|
if (typeof w === "string") {
|
||||||
return w.toLowerCase() === x.solution.toLowerCase();
|
return w.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else if ('letter' in w) {
|
} else if ('letter' in w) {
|
||||||
return w.word.toLowerCase() === x.solution.toLowerCase();
|
return w.letter.toLowerCase() === x.solution.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
return w.id.toString() === x.id.toString();
|
return w.id.toString() === x.id.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Stat } from "@/interfaces/user";
|
|||||||
export interface ITrainingContent {
|
export interface ITrainingContent {
|
||||||
id: string;
|
id: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
|
user: string;
|
||||||
exams: {
|
exams: {
|
||||||
id: string;
|
id: string;
|
||||||
date: number;
|
date: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type} from "@/interfaces/user";
|
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type, Stat} from "@/interfaces/user";
|
||||||
import {groupBySession, averageScore} from "@/utils/stats";
|
import {groupBySession, averageScore} from "@/utils/stats";
|
||||||
import {RadioGroup} from "@headlessui/react";
|
import {RadioGroup} from "@headlessui/react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -122,7 +122,7 @@ const UserCard = ({
|
|||||||
const [commissionValue, setCommission] = useState(
|
const [commissionValue, setCommission] = useState(
|
||||||
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.commission : undefined,
|
user.type === "corporate" || user.type === "mastercorporate" ? user.corporateInformation?.payment?.commission : undefined,
|
||||||
);
|
);
|
||||||
const {stats} = useStats(user.id);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id);
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {codes} = useCodes(user.id);
|
const {codes} = useCodes(user.id);
|
||||||
const {permissions} = usePermissions(loggedInUser.id);
|
const {permissions} = usePermissions(loggedInUser.id);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {User} from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
import {dateSorter} from "@/utils";
|
import {dateSorter} from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -36,7 +36,7 @@ export default function AdminDashboard({user}: Props) {
|
|||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
const {stats} = useStats(user.id);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id);
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const {groups} = useGroups({});
|
const {groups} = useGroups({});
|
||||||
const {pending, done} = usePaymentStatusUsers();
|
const {pending, done} = usePaymentStatusUsers();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {User} from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
import {dateSorter} from "@/utils";
|
import {dateSorter} from "@/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -23,7 +23,7 @@ export default function AgentDashboard({user}: Props) {
|
|||||||
const [selectedUser, setSelectedUser] = useState<User>();
|
const [selectedUser, setSelectedUser] = useState<User>();
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
const {stats} = useStats();
|
const {data: stats} = useFilterRecordsByUser<Stat[]>();
|
||||||
const {users, reload} = useUsers();
|
const {users, reload} = useUsers();
|
||||||
const {pending, done} = usePaymentStatusUsers();
|
const {pending, done} = usePaymentStatusUsers();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
|
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
@@ -232,7 +232,7 @@ export default function CorporateDashboard({ user }: Props) {
|
|||||||
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
|
||||||
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
|
||||||
|
|
||||||
const { stats } = useStats();
|
const { data: stats } = useFilterRecordsByUser<Stat[]>();
|
||||||
const { users, reload, isLoading } = useUsers();
|
const { users, reload, isLoading } = useUsers();
|
||||||
const { codes } = useCodes(user.id);
|
const { codes } = useCodes(user.id);
|
||||||
const { groups } = useGroups({ admin: user.id });
|
const { groups } = useGroups({ admin: user.id });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {
|
import {
|
||||||
CorporateUser,
|
CorporateUser,
|
||||||
@@ -435,7 +435,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
|
|||||||
(Assignment & { corporate?: CorporateUser })[]
|
(Assignment & { corporate?: CorporateUser })[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const { stats } = useStats();
|
const { data: stats } = useFilterRecordsByUser<Stat[]>();
|
||||||
const { users, reload } = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const { codes } = useCodes(user.id);
|
const { codes } = useCodes(user.id);
|
||||||
const { groups } = useGroups({ admin: user.id, userType: user.type });
|
const { groups } = useGroups({ admin: user.id, userType: user.type });
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import ProfileSummary from "@/components/ProfileSummary";
|
|||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import useGradingSystem from "@/hooks/useGrading";
|
import useGradingSystem from "@/hooks/useGrading";
|
||||||
import useInvites from "@/hooks/useInvites";
|
import useInvites from "@/hooks/useInvites";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import {Invite} from "@/interfaces/invite";
|
import {Invite} from "@/interfaces/invite";
|
||||||
import {Assignment} from "@/interfaces/results";
|
import {Assignment} from "@/interfaces/results";
|
||||||
import {CorporateUser, User} from "@/interfaces/user";
|
import {CorporateUser, Stat, User} from "@/interfaces/user";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {getExamById} from "@/utils/exams";
|
import {getExamById} from "@/utils/exams";
|
||||||
import {getUserCorporate} from "@/utils/groups";
|
import {getUserCorporate} from "@/utils/groups";
|
||||||
@@ -37,7 +37,7 @@ export default function StudentDashboard({user}: Props) {
|
|||||||
|
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {gradingSystem} = useGradingSystem();
|
const {gradingSystem} = useGradingSystem();
|
||||||
const {stats} = useStats(user.id, !user?.id);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id, !user?.id);
|
||||||
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id});
|
||||||
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
|
const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
|
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
|
||||||
import UserList from "@/pages/(admin)/Lists/UserList";
|
import UserList from "@/pages/(admin)/Lists/UserList";
|
||||||
@@ -68,7 +68,7 @@ export default function TeacherDashboard({ user }: Props) {
|
|||||||
const [corporateUserToShow, setCorporateUserToShow] =
|
const [corporateUserToShow, setCorporateUserToShow] =
|
||||||
useState<CorporateUser>();
|
useState<CorporateUser>();
|
||||||
|
|
||||||
const { stats } = useStats();
|
const { data: stats } = useFilterRecordsByUser<Stat[]>();
|
||||||
const { users, reload } = useUsers();
|
const { users, reload } = useUsers();
|
||||||
const { groups } = useGroups({ adminAdmins: user.id });
|
const { groups } = useGroups({ adminAdmins: user.id });
|
||||||
const { permissions } = usePermissions(user.id);
|
const { permissions } = usePermissions(user.id);
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Module} from "@/interfaces";
|
import {Module} from "@/interfaces";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {User} from "@/interfaces/user";
|
import {Stat, User} from "@/interfaces/user";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
|
||||||
import {totalExamsByModule} from "@/utils/stats";
|
import {totalExamsByModule} from "@/utils/stats";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import {calculateAverageLevel} from "@/utils/score";
|
import {calculateAverageLevel} from "@/utils/score";
|
||||||
import {sortByModuleName} from "@/utils/moduleUtils";
|
import {sortByModuleName} from "@/utils/moduleUtils";
|
||||||
@@ -30,7 +30,7 @@ export default function Selection({user, page, onStart, disableSelection = false
|
|||||||
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
|
||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
|
|
||||||
const {stats} = useStats(user?.id);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(user?.id);
|
||||||
const {sessions, isLoading, reload} = useSessions(user.id);
|
const {sessions, isLoading, reload} = useSessions(user.id);
|
||||||
|
|
||||||
const state = useExamStore((state) => state);
|
const state = useExamStore((state) => state);
|
||||||
|
|||||||
51
src/hooks/useFilterRecordsByUser.tsx
Normal file
51
src/hooks/useFilterRecordsByUser.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const endpoints: Record<string, string> = {
|
||||||
|
stats: "/api/stats",
|
||||||
|
training: "/api/training"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useFilterRecordsByUser<T extends any[]>(
|
||||||
|
id?: string,
|
||||||
|
shouldNotQuery?: boolean,
|
||||||
|
recordType: 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 endpointURL = endpoints[recordType] || endpoints.stats;
|
||||||
|
// CAUTION: This makes the assumption that the record enpoint has a /user/${id} endpoint
|
||||||
|
const endpoint = !id ? endpointURL: `${endpointURL}/user/${id}`;
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
if (shouldNotQuery) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setIsError(false);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get<T>(endpoint)
|
||||||
|
.then((response) => {
|
||||||
|
// CAUTION: This makes the assumption ElementType has a "user" field that contains the user id
|
||||||
|
setData(response.data.filter((x: ElementType) => (id ? (x as any).user === id : true)) as T);
|
||||||
|
})
|
||||||
|
.catch(() => setIsError(true))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [id, shouldNotQuery, recordType, endpoint]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
reload: getData,
|
||||||
|
isLoading,
|
||||||
|
isError
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import {Stat, User} from "@/interfaces/user";
|
|
||||||
import axios from "axios";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
|
|
||||||
export default function useStats(id?: string, shouldNotQuery?: boolean) {
|
|
||||||
const [stats, setStats] = useState<Stat[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [isError, setIsError] = useState(false);
|
|
||||||
|
|
||||||
const getData = () => {
|
|
||||||
if (shouldNotQuery) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
axios
|
|
||||||
.get<Stat[]>(!id ? "/api/stats" : `/api/stats/user/${id}`)
|
|
||||||
.then((response) => setStats(response.data.filter((x) => (id ? x.user === id : true))))
|
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(getData, [id, shouldNotQuery]);
|
|
||||||
|
|
||||||
return {stats, reload: getData, isLoading, isError};
|
|
||||||
}
|
|
||||||
29
src/pages/api/training/user/[user].ts
Normal file
29
src/pages/api/training/user/[user].ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type {NextApiRequest, NextApiResponse} from "next";
|
||||||
|
import {app} from "@/firebase";
|
||||||
|
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc} from "firebase/firestore";
|
||||||
|
import {withIronSessionApiRoute} from "iron-session/next";
|
||||||
|
import {sessionOptions} from "@/lib/session";
|
||||||
|
|
||||||
|
const db = getFirestore(app);
|
||||||
|
|
||||||
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session.user) {
|
||||||
|
res.status(401).json({ok: false});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {user} = req.query;
|
||||||
|
const q = query(collection(db, "training"), where("user", "==", user));
|
||||||
|
|
||||||
|
const snapshot = await getDocs(q);
|
||||||
|
|
||||||
|
res.status(200).json(
|
||||||
|
snapshot.docs.map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import {BsFileEarmarkText, BsPencil, BsStar, BsBook, BsHeadphones, BsPen, BsMega
|
|||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import useStats from "@/hooks/useStats";
|
|
||||||
import {averageScore, groupBySession, totalExams} from "@/utils/stats";
|
import {averageScore, groupBySession, totalExams} from "@/utils/stats";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import Diagnostic from "@/components/Diagnostic";
|
import Diagnostic from "@/components/Diagnostic";
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useStats from "@/hooks/useStats";
|
|
||||||
import { averageScore, groupBySession, totalExams } from "@/utils/stats";
|
import { averageScore, groupBySession, totalExams } from "@/utils/stats";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import Diagnostic from "@/components/Diagnostic";
|
import Diagnostic from "@/components/Diagnostic";
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
/* 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 {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {Stat, User} from "@/interfaces/user";
|
import { Stat, User } from "@/interfaces/user";
|
||||||
import {useEffect, useRef, useState} from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import {groupByDate} from "@/utils/stats";
|
import { groupByDate } from "@/utils/stats";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {ToastContainer} from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import {useRouter} from "next/router";
|
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Select from "@/components/Low/Select";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import useGroups from "@/hooks/useGroups";
|
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
import {uuidv4} from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
import { usePDFDownload } from "@/hooks/usePDFDownload";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
|
import StatsGridItem from "@/components/Medium/StatGridItem";
|
||||||
|
import RecordFilter from "@/components/Medium/RecordFilter";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import StatsGridItem from "@/components/StatGridItem";
|
|
||||||
import {checkAccess} from "@/utils/permissions";
|
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
@@ -46,16 +44,14 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user: req.session.user},
|
props: { user: req.session.user },
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const defaultSelectableCorporate = {
|
type Filter = "months" | "weeks" | "days" | "assignments" | undefined;
|
||||||
value: "",
|
|
||||||
label: "All",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function History({user}: {user: User}) {
|
export default function History({ user }: { user: User }) {
|
||||||
|
const router = useRouter();
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
state.setSelectedUser,
|
state.setSelectedUser,
|
||||||
@@ -64,14 +60,12 @@ export default function History({user}: {user: User}) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
|
||||||
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
|
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
const [filter, setFilter] = useState<Filter>();
|
||||||
const {assignments} = useAssignments({});
|
const { assignments } = useAssignments({});
|
||||||
|
|
||||||
const {users} = useUsers();
|
const { users } = useUsers();
|
||||||
const {stats, isLoading: isStatsLoading} = useStats(statsUserId || user?.id);
|
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
|
||||||
const {groups: allGroups} = useGroups({});
|
|
||||||
const {groups} = useGroups({admin: user?.id, userType: user?.type});
|
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
|
||||||
@@ -80,7 +74,6 @@ export default function History({user}: {user: User}) {
|
|||||||
const setInactivity = useExamStore((state) => state.setInactivity);
|
const setInactivity = useExamStore((state) => state.setInactivity);
|
||||||
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
|
||||||
const renderPdfIcon = usePDFDownload("stats");
|
const renderPdfIcon = usePDFDownload("stats");
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
|
||||||
|
|
||||||
@@ -107,16 +100,12 @@ export default function History({user}: {user: User}) {
|
|||||||
// if (!statsUserId) setStatsUserId(user.id);
|
// if (!statsUserId) setStatsUserId(user.id);
|
||||||
// }, []);
|
// }, []);
|
||||||
|
|
||||||
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
|
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
|
||||||
setFilter((prev) => (prev === value ? undefined : value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterStatsByDate = (stats: {[key: string]: Stat[]}) => {
|
|
||||||
if (filter && filter !== "assignments") {
|
if (filter && filter !== "assignments") {
|
||||||
const filterDate = moment()
|
const filterDate = moment()
|
||||||
.subtract({[filter as string]: 1})
|
.subtract({ [filter as string]: 1 })
|
||||||
.format("x");
|
.format("x");
|
||||||
const filteredStats: {[key: string]: Stat[]} = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
if (timestamp >= filterDate) filteredStats[timestamp] = stats[timestamp];
|
||||||
@@ -125,7 +114,7 @@ export default function History({user}: {user: User}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filter && filter === "assignments") {
|
if (filter && filter === "assignments") {
|
||||||
const filteredStats: {[key: string]: Stat[]} = {};
|
const filteredStats: { [key: string]: Stat[] } = {};
|
||||||
|
|
||||||
Object.keys(stats).forEach((timestamp) => {
|
Object.keys(stats).forEach((timestamp) => {
|
||||||
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
if (stats[timestamp].map((s) => s.assignment === undefined).includes(false))
|
||||||
@@ -138,20 +127,11 @@ export default function History({user}: {user: User}) {
|
|||||||
return stats;
|
return stats;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const MAX_TRAINING_EXAMS = 10;
|
const MAX_TRAINING_EXAMS = 10;
|
||||||
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
|
||||||
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
const setTrainingStats = useTrainingContentStore((state) => state.setStats);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleRouteChange = (url: string) => {
|
|
||||||
setTraining(false);
|
|
||||||
};
|
|
||||||
router.events.on("routeChangeStart", handleRouteChange);
|
|
||||||
return () => {
|
|
||||||
router.events.off("routeChangeStart", handleRouteChange);
|
|
||||||
};
|
|
||||||
}, [router.events, setTraining]);
|
|
||||||
|
|
||||||
const handleTrainingContentSubmission = () => {
|
const handleTrainingContentSubmission = () => {
|
||||||
if (groupedStats) {
|
if (groupedStats) {
|
||||||
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
const groupedStatsByDate = filterStatsByDate(groupedStats);
|
||||||
@@ -168,6 +148,16 @@ export default function History({user}: {user: User}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRouteChange = (url: string) => {
|
||||||
|
setTraining(false);
|
||||||
|
};
|
||||||
|
router.events.on("routeChangeStart", handleRouteChange);
|
||||||
|
return () => {
|
||||||
|
router.events.off("routeChangeStart", handleRouteChange);
|
||||||
|
};
|
||||||
|
}, [router.events, setTraining]);
|
||||||
|
|
||||||
const customContent = (timestamp: string) => {
|
const customContent = (timestamp: string) => {
|
||||||
if (!groupedStats) return <></>;
|
if (!groupedStats) return <></>;
|
||||||
|
|
||||||
@@ -196,52 +186,6 @@ export default function History({user}: {user: User}) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectableCorporates = [
|
|
||||||
defaultSelectableCorporate,
|
|
||||||
...users
|
|
||||||
.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id))
|
|
||||||
.filter((x) => x.type === "corporate")
|
|
||||||
.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
|
||||||
|
|
||||||
const getUsersList = (): User[] => {
|
|
||||||
if (selectedCorporate) {
|
|
||||||
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
|
|
||||||
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
|
||||||
|
|
||||||
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
|
||||||
return userListWithUsers.filter((x) => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.type !== "mastercorporate" ? users : users.filter((x) => groups.flatMap((g) => [g.admin, ...g.participants]).includes(x.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const corporateFilteredUserList = getUsersList();
|
|
||||||
|
|
||||||
const getSelectedUser = () => {
|
|
||||||
if (selectedCorporate) {
|
|
||||||
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
|
||||||
return userInCorporate || corporateFilteredUserList[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.find((x) => x.id === statsUserId) || user;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedUser = getSelectedUser();
|
|
||||||
const selectedUserSelectValue = selectedUser
|
|
||||||
? {
|
|
||||||
value: selectedUser.id,
|
|
||||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: "",
|
|
||||||
label: "",
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -256,68 +200,7 @@ export default function History({user}: {user: User}) {
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
{user && (
|
{user && (
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
<RecordFilter user={user} filterState={{ filter: filter, setFilter: setFilter }} >
|
||||||
<div className="xl:w-3/4">
|
|
||||||
{checkAccess(user, ["developer", "admin", "mastercorporate"]) && !training && (
|
|
||||||
<>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={selectableCorporates}
|
|
||||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
|
||||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}></Select>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={corporateFilteredUserList.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
}))}
|
|
||||||
value={selectedUserSelectValue}
|
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && !training && (
|
|
||||||
<>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={users
|
|
||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
|
||||||
.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
}))}
|
|
||||||
value={selectedUserSelectValue}
|
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{training && (
|
{training && (
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="font-semibold text-2xl mr-4">
|
<div className="font-semibold text-2xl mr-4">
|
||||||
@@ -335,46 +218,7 @@ export default function History({user}: {user: User}) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</RecordFilter>
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "assignments" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("assignments")}>
|
|
||||||
Assignments
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "months" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("months")}>
|
|
||||||
Last month
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "weeks" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("weeks")}>
|
|
||||||
Last week
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "days" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("days")}>
|
|
||||||
Last day
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && (
|
{groupedStats && Object.keys(groupedStats).length > 0 && !isStatsLoading && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||||
{Object.keys(filterStatsByDate(groupedStats))
|
{Object.keys(filterStatsByDate(groupedStats))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {LinearScale, Chart as ChartJS, CategoryScale, PointElement, LineElement,
|
|||||||
import {withIronSessionSsr} from "iron-session/next";
|
import {withIronSessionSsr} from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import useStats from "@/hooks/useStats";
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
import {averageScore, totalExamsByModule, groupBySession, groupByModule, timestampToMoment, groupByDate} from "@/utils/stats";
|
import {averageScore, totalExamsByModule, groupBySession, groupByModule, timestampToMoment, groupByDate} from "@/utils/stats";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
@@ -72,7 +72,7 @@ export default function Stats() {
|
|||||||
const {user} = useUser({redirectTo: "/login"});
|
const {user} = useUser({redirectTo: "/login"});
|
||||||
const {users} = useUsers();
|
const {users} = useUsers();
|
||||||
const {groups} = useGroups({admin: user?.id});
|
const {groups} = useGroups({admin: user?.id});
|
||||||
const {stats} = useStats(statsUserId, !statsUserId);
|
const {data: stats} = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) setStatsUserId(user.id);
|
if (user) setStatsUserId(user.id);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {withIronSessionSsr} from "iron-session/next";
|
|||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import {sessionOptions} from "@/lib/session";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
import StatsGridItem from "@/components/StatGridItem";
|
import StatsGridItem from "@/components/Medium/StatGridItem";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
import {usePDFDownload} from "@/hooks/usePDFDownload";
|
||||||
import useAssignments from "@/hooks/useAssignments";
|
import useAssignments from "@/hooks/useAssignments";
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
/* 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 {withIronSessionSsr} from "iron-session/next";
|
import { withIronSessionSsr } from "iron-session/next";
|
||||||
import {sessionOptions} from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import {Stat, 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 Layout from "@/components/High/Layout";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import {use, useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {FaPlus} from "react-icons/fa";
|
import { FaPlus } from "react-icons/fa";
|
||||||
import useRecordStore from "@/stores/recordStore";
|
import useRecordStore from "@/stores/recordStore";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
import useTrainingContentStore from "@/stores/trainingContentStore";
|
import useTrainingContentStore from "@/stores/trainingContentStore";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Select from "@/components/Low/Select";
|
import { ITrainingContent } from "@/training/TrainingInterfaces";
|
||||||
import useUsers from "@/hooks/useUsers";
|
|
||||||
import useGroups from "@/hooks/useGroups";
|
|
||||||
import {ITrainingContent} from "@/training/TrainingInterfaces";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {uuidv4} from "@firebase/util";
|
import { uuidv4 } from "@firebase/util";
|
||||||
import TrainingScore from "@/training/TrainingScore";
|
import TrainingScore from "@/training/TrainingScore";
|
||||||
import ModuleBadge from "@/components/ModuleBadge";
|
import ModuleBadge from "@/components/ModuleBadge";
|
||||||
|
import RecordFilter from "@/components/Medium/RecordFilter";
|
||||||
|
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
|
|
||||||
if (!user || !user.isVerified) {
|
if (!user || !user.isVerified) {
|
||||||
@@ -44,37 +43,22 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {user: req.session.user},
|
props: { user: req.session.user },
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
const defaultSelectableCorporate = {
|
const Training: React.FC<{ user: User }> = ({ user }) => {
|
||||||
value: "",
|
const [recordUserId, setRecordTraining] = useRecordStore((state) => [
|
||||||
label: "All",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Training: React.FC<{user: User}> = ({user}) => {
|
|
||||||
// Record stuff
|
|
||||||
const {users} = useUsers();
|
|
||||||
const [selectedCorporate, setSelectedCorporate] = useState<string>(defaultSelectableCorporate.value);
|
|
||||||
const [statsUserId, setStatsUserId, setRecordTraining] = useRecordStore((state) => [
|
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
state.setSelectedUser,
|
|
||||||
state.setTraining,
|
state.setTraining,
|
||||||
]);
|
]);
|
||||||
const {groups: allGroups} = useGroups({});
|
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
|
||||||
const groups = allGroups.filter((x) => x.admin === user.id);
|
|
||||||
const [filter, setFilter] = useState<"months" | "weeks" | "days">();
|
|
||||||
|
|
||||||
const toggleFilter = (value: "months" | "weeks" | "days") => {
|
|
||||||
setFilter((prev) => (prev === value ? undefined : value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
|
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
|
||||||
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
|
|
||||||
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
|
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>();
|
||||||
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{[key: string]: ITrainingContent}>();
|
|
||||||
|
const { data: trainingContent, isLoading: areRecordsLoading } = useFilterRecordsByUser<ITrainingContent[]>(recordUserId || user?.id, undefined, "training");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = (url: string) => {
|
const handleRouteChange = (url: string) => {
|
||||||
@@ -90,7 +74,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const postStats = async () => {
|
const postStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<{id: string}>(`/api/training`, stats);
|
const response = await axios.post<{ id: string }>(`/api/training`, { userID: user.id, stats: stats });
|
||||||
return response.data.id;
|
return response.data.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsNewContentLoading(false);
|
setIsNewContentLoading(false);
|
||||||
@@ -108,31 +92,17 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isNewContentLoading]);
|
}, [isNewContentLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadTrainingContent = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get<ITrainingContent[]>("/api/training");
|
|
||||||
setTrainingContent(response.data);
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
setTrainingContent([]);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadTrainingContent();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleNewTrainingContent = () => {
|
const handleNewTrainingContent = () => {
|
||||||
setRecordTraining(true);
|
setRecordTraining(true);
|
||||||
router.push("/record");
|
router.push("/record");
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTrainingContentByDate = (trainingContent: {[key: string]: ITrainingContent}) => {
|
const filterTrainingContentByDate = (trainingContent: { [key: string]: ITrainingContent }) => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const filterDate = moment()
|
const filterDate = moment()
|
||||||
.subtract({[filter as string]: 1})
|
.subtract({ [filter as string]: 1 })
|
||||||
.format("x");
|
.format("x");
|
||||||
const filteredTrainingContent: {[key: string]: ITrainingContent} = {};
|
const filteredTrainingContent: { [key: string]: ITrainingContent } = {};
|
||||||
|
|
||||||
Object.keys(trainingContent).forEach((timestamp) => {
|
Object.keys(trainingContent).forEach((timestamp) => {
|
||||||
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
if (timestamp >= filterDate) filteredTrainingContent[timestamp] = trainingContent[timestamp];
|
||||||
@@ -147,68 +117,14 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
const grouped = trainingContent.reduce((acc, content) => {
|
const grouped = trainingContent.reduce((acc, content) => {
|
||||||
acc[content.created_at] = content;
|
acc[content.created_at] = content;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as {[key: number]: ITrainingContent});
|
}, {} as { [key: number]: ITrainingContent });
|
||||||
|
|
||||||
setGroupedByTrainingContent(grouped);
|
setGroupedByTrainingContent(grouped);
|
||||||
|
}else {
|
||||||
|
setGroupedByTrainingContent(undefined);
|
||||||
}
|
}
|
||||||
}, [trainingContent]);
|
}, [trainingContent]);
|
||||||
|
|
||||||
// Record Stuff
|
|
||||||
const selectableCorporates = [
|
|
||||||
defaultSelectableCorporate,
|
|
||||||
...users
|
|
||||||
.filter((x) => x.type === "corporate")
|
|
||||||
.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
const getUsersList = (): User[] => {
|
|
||||||
if (selectedCorporate) {
|
|
||||||
// get groups for that corporate
|
|
||||||
const selectedCorporateGroups = allGroups.filter((x) => x.admin === selectedCorporate);
|
|
||||||
|
|
||||||
// get the teacher ids for that group
|
|
||||||
const selectedCorporateGroupsParticipants = selectedCorporateGroups.flatMap((x) => x.participants);
|
|
||||||
|
|
||||||
// // search for groups for these teachers
|
|
||||||
// const teacherGroups = allGroups.filter((x) => {
|
|
||||||
// return selectedCorporateGroupsParticipants.includes(x.admin);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const usersList = [
|
|
||||||
// ...selectedCorporateGroupsParticipants,
|
|
||||||
// ...teacherGroups.flatMap((x) => x.participants),
|
|
||||||
// ];
|
|
||||||
const userListWithUsers = selectedCorporateGroupsParticipants.map((x) => users.find((y) => y.id === x)) as User[];
|
|
||||||
return userListWithUsers.filter((x) => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
return users || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const corporateFilteredUserList = getUsersList();
|
|
||||||
const getSelectedUser = () => {
|
|
||||||
if (selectedCorporate) {
|
|
||||||
const userInCorporate = corporateFilteredUserList.find((x) => x.id === statsUserId);
|
|
||||||
return userInCorporate || corporateFilteredUserList[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.find((x) => x.id === statsUserId) || user;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedUser = getSelectedUser();
|
|
||||||
const selectedUserSelectValue = selectedUser
|
|
||||||
? {
|
|
||||||
value: selectedUser.id,
|
|
||||||
label: `${selectedUser.name} - ${selectedUser.email}`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: "",
|
|
||||||
label: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) => {
|
const formatTimestamp = (timestamp: string) => {
|
||||||
const date = moment(parseInt(timestamp));
|
const date = moment(parseInt(timestamp));
|
||||||
const formatter = "YYYY/MM/DD - HH:mm";
|
const formatter = "YYYY/MM/DD - HH:mm";
|
||||||
@@ -222,6 +138,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
|
|
||||||
const trainingContentContainer = (timestamp: string) => {
|
const trainingContentContainer = (timestamp: string) => {
|
||||||
if (!groupedByTrainingContent) return <></>;
|
if (!groupedByTrainingContent) return <></>;
|
||||||
|
|
||||||
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
|
||||||
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
|
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
|
||||||
|
|
||||||
@@ -266,7 +183,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
<Layout user={user}>
|
<Layout user={user}>
|
||||||
{isNewContentLoading || isLoading ? (
|
{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" />
|
||||||
{isNewContentLoading && (
|
{isNewContentLoading && (
|
||||||
@@ -275,68 +192,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
|
<RecordFilter user={user} filterState={{ filter: filter, setFilter: setFilter }} assignments={false} >
|
||||||
<div className="xl:w-3/4">
|
|
||||||
{(user.type === "developer" || user.type === "admin") && (
|
|
||||||
<>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Corporate</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={selectableCorporates}
|
|
||||||
value={selectableCorporates.find((x) => x.value === selectedCorporate)}
|
|
||||||
onChange={(value) => setSelectedCorporate(value?.value || "")}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}></Select>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={corporateFilteredUserList.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
}))}
|
|
||||||
value={selectedUserSelectValue}
|
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(user.type === "corporate" || user.type === "teacher") && groups.length > 0 && (
|
|
||||||
<>
|
|
||||||
<label className="font-normal text-base text-mti-gray-dim">User</label>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={users
|
|
||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
|
||||||
.map((x) => ({
|
|
||||||
value: x.id,
|
|
||||||
label: `${x.name} - ${x.email}`,
|
|
||||||
}))}
|
|
||||||
value={selectedUserSelectValue}
|
|
||||||
onChange={(value) => setStatsUserId(value?.value!)}
|
|
||||||
styles={{
|
|
||||||
menuPortal: (base) => ({...base, zIndex: 9999}),
|
|
||||||
option: (styles, state) => ({
|
|
||||||
...styles,
|
|
||||||
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
|
|
||||||
color: state.isFocused ? "black" : styles.color,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{user.type === "student" && (
|
{user.type === "student" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -352,43 +208,13 @@ const Training: React.FC<{user: User}> = ({user}) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</RecordFilter>
|
||||||
<div className="flex gap-4 w-full justify-center xl:justify-end">
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "months" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("months")}>
|
|
||||||
Last month
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "weeks" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("weeks")}>
|
|
||||||
Last week
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
|
|
||||||
"transition duration-300 ease-in-out",
|
|
||||||
filter === "days" && "!bg-mti-purple-light !text-white",
|
|
||||||
)}
|
|
||||||
onClick={() => toggleFilter("days")}>
|
|
||||||
Last day
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{trainingContent.length == 0 && (
|
{trainingContent.length == 0 && (
|
||||||
<div className="flex flex-grow justify-center items-center">
|
<div className="flex flex-grow justify-center items-center">
|
||||||
<span className="font-semibold ml-1">No training content to display...</span>
|
<span className="font-semibold ml-1">No training content to display...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
{!areRecordsLoading && groupedByTrainingContent && Object.keys(groupedByTrainingContent).length > 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-3 w-full gap-4 xl:gap-6">
|
||||||
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
|
||||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
|
|||||||
Reference in New Issue
Block a user