Merged in feature/training-content (pull request #80)

Feature/training content

Approved-by: Tiago Ribeiro
This commit is contained in:
carlos.mesquita
2024-08-27 10:03:53 +00:00
committed by Tiago Ribeiro
22 changed files with 447 additions and 506 deletions

View File

@@ -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) ? (
<> <>

View 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;

View File

@@ -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;

View File

@@ -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();
} }

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -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});

View File

@@ -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);

View File

@@ -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);

View 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
};
}

View File

@@ -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};
}

View 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(),
})),
);
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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))

View File

@@ -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);

View File

@@ -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";

View File

@@ -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))