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 useExamStore from "@/stores/examStore";
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 { CommonProps } from "..";
import Button from "../../Low/Button";
@@ -45,7 +45,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
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;
}
@@ -55,10 +55,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution;
if (!solution) return false;
const option = correctWords!.find((w: any) => {
console.log(w);
if (typeof w === "string") {
return w.toLowerCase() === x.solution.toLowerCase();
} else if ('letter' in w) {
return w.word.toLowerCase() === x.solution.toLowerCase();
return w.letter.toLowerCase() === x.solution.toLowerCase();
} else {
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;
return { total, correct, missing };
};
const renderLines = (line: string) => {
const renderLines = useCallback((line: string) => {
return (
<div className="text-base leading-5" key={v4()}>
{reactStringReplace(line, /({{\d+}})/g, (match) => {
@@ -121,21 +122,34 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
})}
</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) => {
setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), { id: questionID, solution: value }]);
}
useEffect(() => {
//if (variant === "mc") {
console.log(answers);
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
//}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers])
return (
<>
<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) => (
<Fragment key={index}>
{line}
@@ -144,12 +158,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
))}
</span>}
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
{text.split("\\n").map((line, index) => (
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
{renderLines(line)}
<br />
</p>
))}
{memoizedLines}
</span>
{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

@@ -5,16 +5,16 @@ import {Stat, User} from "@/interfaces/user";
import {Module} from "@/interfaces";
import ai_usage from "@/utils/ai.detection";
import { calculateBandScore } from "@/utils/score";
import moment from "moment";
import {Assignment} from "@/interfaces/results";
import moment from 'moment';
import { Assignment } from '@/interfaces/results';
import { uuidv4 } from "@firebase/util";
import { useRouter } from "next/router";
import { uniqBy } from "lodash";
import { sortByModule } from "@/utils/moduleUtils";
import { convertToUserSolutions } from "@/utils/stats";
import { getExamById } from "@/utils/exams";
import {Exam, UserSolution} from "@/interfaces/exam";
import ModuleBadge from "./ModuleBadge";
import { Exam, UserSolution } from '@/interfaces/exam';
import ModuleBadge from '../ModuleBadge';
const formatTimestamp = (timestamp: string | number) => {
const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp;

View File

@@ -34,7 +34,7 @@ export default function FillBlanksSolutions({
if (typeof w === "string") {
return w.toLowerCase() === x.solution.toLowerCase();
} else if ('letter' in w) {
return w.word.toLowerCase() === x.solution.toLowerCase();
return w.letter.toLowerCase() === x.solution.toLowerCase();
} else {
return w.id.toString() === x.id.toString();
}

View File

@@ -3,6 +3,7 @@ import { Stat } from "@/interfaces/user";
export interface ITrainingContent {
id: string;
created_at: number;
user: string;
exams: {
id: string;
date: number;

View File

@@ -1,5 +1,5 @@
import useStats from "@/hooks/useStats";
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type} from "@/interfaces/user";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import {CorporateInformation, CorporateUser, EMPLOYMENT_STATUS, User, Type, Stat} from "@/interfaces/user";
import {groupBySession, averageScore} from "@/utils/stats";
import {RadioGroup} from "@headlessui/react";
import axios from "axios";
@@ -122,7 +122,7 @@ const UserCard = ({
const [commissionValue, setCommission] = useState(
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 {codes} = useCodes(user.id);
const {permissions} = usePermissions(loggedInUser.id);

View File

@@ -1,8 +1,8 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {User} from "@/interfaces/user";
import {Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter} from "@/utils";
import moment from "moment";
@@ -36,7 +36,7 @@ export default function AdminDashboard({user}: Props) {
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const {stats} = useStats(user.id);
const {data: stats} = useFilterRecordsByUser<Stat[]>(user.id);
const {users, reload} = useUsers();
const {groups} = useGroups({});
const {pending, done} = usePaymentStatusUsers();

View File

@@ -1,8 +1,8 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {User} from "@/interfaces/user";
import {Stat, User} from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
import {dateSorter} from "@/utils";
import moment from "moment";
@@ -23,7 +23,7 @@ export default function AgentDashboard({user}: Props) {
const [selectedUser, setSelectedUser] = useState<User>();
const [showModal, setShowModal] = useState(false);
const {stats} = useStats();
const {data: stats} = useFilterRecordsByUser<Stat[]>();
const {users, reload} = useUsers();
const {pending, done} = usePaymentStatusUsers();

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
@@ -232,7 +232,7 @@ export default function CorporateDashboard({ user }: Props) {
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const { stats } = useStats();
const { data: stats } = useFilterRecordsByUser<Stat[]>();
const { users, reload, isLoading } = useUsers();
const { codes } = useCodes(user.id);
const { groups } = useGroups({ admin: user.id });

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {
CorporateUser,
@@ -435,7 +435,7 @@ export default function MasterCorporateDashboard({ user }: Props) {
(Assignment & { corporate?: CorporateUser })[]
>([]);
const { stats } = useStats();
const { data: stats } = useFilterRecordsByUser<Stat[]>();
const { users, reload } = useUsers();
const { codes } = useCodes(user.id);
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 useGradingSystem from "@/hooks/useGrading";
import useInvites from "@/hooks/useInvites";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import {Invite} from "@/interfaces/invite";
import {Assignment} from "@/interfaces/results";
import {CorporateUser, User} from "@/interfaces/user";
import {CorporateUser, Stat, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams";
import {getUserCorporate} from "@/utils/groups";
@@ -37,7 +37,7 @@ export default function StudentDashboard({user}: Props) {
const {users} = useUsers();
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 {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user.id});

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import useUsers from "@/hooks/useUsers";
import { CorporateUser, Group, Stat, User } from "@/interfaces/user";
import UserList from "@/pages/(admin)/Lists/UserList";
@@ -68,7 +68,7 @@ export default function TeacherDashboard({ user }: Props) {
const [corporateUserToShow, setCorporateUserToShow] =
useState<CorporateUser>();
const { stats } = useStats();
const { data: stats } = useFilterRecordsByUser<Stat[]>();
const { users, reload } = useUsers();
const { groups } = useGroups({ adminAdmins: user.id });
const { permissions } = usePermissions(user.id);

View File

@@ -2,11 +2,11 @@
import {useState} from "react";
import {Module} from "@/interfaces";
import clsx from "clsx";
import {User} from "@/interfaces/user";
import {Stat, User} from "@/interfaces/user";
import ProgressBar from "@/components/Low/ProgressBar";
import {BsArrowRepeat, BsBook, BsCheck, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
import {totalExamsByModule} from "@/utils/stats";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import Button from "@/components/Low/Button";
import {calculateAverageLevel} from "@/utils/score";
import {sortByModuleName} from "@/utils/moduleUtils";
@@ -30,7 +30,7 @@ export default function Selection({user, page, onStart, disableSelection = false
const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true);
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 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 {sessionOptions} from "@/lib/session";
import {useEffect, useState} from "react";
import useStats from "@/hooks/useStats";
import {averageScore, groupBySession, totalExams} from "@/utils/stats";
import useUser from "@/hooks/useUser";
import Diagnostic from "@/components/Diagnostic";

View File

@@ -13,7 +13,6 @@ import {
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { useEffect, useState } from "react";
import useStats from "@/hooks/useStats";
import { averageScore, groupBySession, totalExams } from "@/utils/stats";
import useUser from "@/hooks/useUser";
import Diagnostic from "@/components/Diagnostic";

View File

@@ -4,25 +4,23 @@ import {withIronSessionSsr} from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Stat, User } from "@/interfaces/user";
import { useEffect, useRef, useState } from "react";
import useStats from "@/hooks/useStats";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import { groupByDate } from "@/utils/stats";
import moment from "moment";
import useUsers from "@/hooks/useUsers";
import useExamStore from "@/stores/examStore";
import { ToastContainer } from "react-toastify";
import {useRouter} from "next/router";
import Layout from "@/components/High/Layout";
import clsx from "clsx";
import Select from "@/components/Low/Select";
import useGroups from "@/hooks/useGroups";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import useAssignments from "@/hooks/useAssignments";
import { uuidv4 } from "@firebase/util";
import { usePDFDownload } from "@/hooks/usePDFDownload";
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 StatsGridItem from "@/components/StatGridItem";
import {checkAccess} from "@/utils/permissions";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user;
@@ -50,12 +48,10 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
};
}, sessionOptions);
const defaultSelectableCorporate = {
value: "",
label: "All",
};
type Filter = "months" | "weeks" | "days" | "assignments" | undefined;
export default function History({ user }: { user: User }) {
const router = useRouter();
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser,
@@ -65,13 +61,11 @@ export default function History({user}: {user: User}) {
// const [statsUserId, setStatsUserId] = useState<string | undefined>(user.id);
const [groupedStats, setGroupedStats] = useState<{ [key: string]: Stat[] }>();
const [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
const [filter, setFilter] = useState<Filter>();
const { assignments } = useAssignments({});
const { users } = useUsers();
const {stats, isLoading: isStatsLoading} = useStats(statsUserId || user?.id);
const {groups: allGroups} = useGroups({});
const {groups} = useGroups({admin: user?.id, userType: user?.type});
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
@@ -80,7 +74,6 @@ export default function History({user}: {user: User}) {
const setInactivity = useExamStore((state) => state.setInactivity);
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
const renderPdfIcon = usePDFDownload("stats");
const router = useRouter();
useEffect(() => setStatsUserId(user.id), [setStatsUserId, user]);
@@ -107,10 +100,6 @@ export default function History({user}: {user: User}) {
// if (!statsUserId) setStatsUserId(user.id);
// }, []);
const toggleFilter = (value: "months" | "weeks" | "days" | "assignments") => {
setFilter((prev) => (prev === value ? undefined : value));
};
const filterStatsByDate = (stats: { [key: string]: Stat[] }) => {
if (filter && filter !== "assignments") {
const filterDate = moment()
@@ -138,20 +127,11 @@ export default function History({user}: {user: User}) {
return stats;
};
const MAX_TRAINING_EXAMS = 10;
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
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 = () => {
if (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) => {
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 (
<>
<Head>
@@ -256,68 +200,7 @@ export default function History({user}: {user: User}) {
<ToastContainer />
{user && (
<Layout user={user}>
<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"]) && !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,
}),
}}
/>
</>
)}
<RecordFilter user={user} filterState={{ filter: filter, setFilter: setFilter }} >
{training && (
<div className="flex flex-row">
<div className="font-semibold text-2xl mr-4">
@@ -335,46 +218,7 @@ export default function History({user}: {user: User}) {
</button>
</div>
)}
</div>
<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>
</RecordFilter>
{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">
{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 {sessionOptions} from "@/lib/session";
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 useUser from "@/hooks/useUser";
import {ToastContainer} from "react-toastify";
@@ -72,7 +72,7 @@ export default function Stats() {
const {user} = useUser({redirectTo: "/login"});
const {users} = useUsers();
const {groups} = useGroups({admin: user?.id});
const {stats} = useStats(statsUserId, !statsUserId);
const {data: stats} = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
useEffect(() => {
if (user) setStatsUserId(user.id);

View File

@@ -18,7 +18,7 @@ import {withIronSessionSsr} from "iron-session/next";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {sessionOptions} from "@/lib/session";
import qs from "qs";
import StatsGridItem from "@/components/StatGridItem";
import StatsGridItem from "@/components/Medium/StatGridItem";
import useExamStore from "@/stores/examStore";
import {usePDFDownload} from "@/hooks/usePDFDownload";
import useAssignments from "@/hooks/useAssignments";

View File

@@ -2,25 +2,24 @@
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import {Stat, User} from "@/interfaces/user";
import { User } from "@/interfaces/user";
import { ToastContainer } from "react-toastify";
import Layout from "@/components/High/Layout";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import {use, useEffect, useState} from "react";
import { useEffect, useState } from "react";
import clsx from "clsx";
import { FaPlus } from "react-icons/fa";
import useRecordStore from "@/stores/recordStore";
import router from "next/router";
import useTrainingContentStore from "@/stores/trainingContentStore";
import axios from "axios";
import Select from "@/components/Low/Select";
import useUsers from "@/hooks/useUsers";
import useGroups from "@/hooks/useGroups";
import { ITrainingContent } from "@/training/TrainingInterfaces";
import moment from "moment";
import { uuidv4 } from "@firebase/util";
import TrainingScore from "@/training/TrainingScore";
import ModuleBadge from "@/components/ModuleBadge";
import RecordFilter from "@/components/Medium/RecordFilter";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
export const getServerSideProps = withIronSessionSsr(({ req, res }) => {
const user = req.session.user;
@@ -48,34 +47,19 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
};
}, sessionOptions);
const defaultSelectableCorporate = {
value: "",
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) => [
const [recordUserId, setRecordTraining] = useRecordStore((state) => [
state.selectedUser,
state.setSelectedUser,
state.setTraining,
]);
const {groups: allGroups} = useGroups({});
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 [filter, setFilter] = useState<"months" | "weeks" | "days" | "assignments">();
const [stats, setTrainingStats] = useTrainingContentStore((state) => [state.stats, state.setStats]);
const [trainingContent, setTrainingContent] = useState<ITrainingContent[]>([]);
const [isNewContentLoading, setIsNewContentLoading] = useState(stats.length != 0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [groupedByTrainingContent, setGroupedByTrainingContent] = useState<{ [key: string]: ITrainingContent }>();
const { data: trainingContent, isLoading: areRecordsLoading } = useFilterRecordsByUser<ITrainingContent[]>(recordUserId || user?.id, undefined, "training");
useEffect(() => {
const handleRouteChange = (url: string) => {
setTrainingStats([]);
@@ -90,7 +74,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
useEffect(() => {
const postStats = async () => {
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;
} catch (error) {
setIsNewContentLoading(false);
@@ -108,20 +92,6 @@ const Training: React.FC<{user: User}> = ({user}) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 = () => {
setRecordTraining(true);
router.push("/record");
@@ -150,65 +120,11 @@ const Training: React.FC<{user: User}> = ({user}) => {
}, {} as { [key: number]: ITrainingContent });
setGroupedByTrainingContent(grouped);
}else {
setGroupedByTrainingContent(undefined);
}
}, [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 date = moment(parseInt(timestamp));
const formatter = "YYYY/MM/DD - HH:mm";
@@ -222,6 +138,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
const trainingContentContainer = (timestamp: string) => {
if (!groupedByTrainingContent) return <></>;
const trainingContent: ITrainingContent = groupedByTrainingContent[timestamp];
const uniqueModules = [...new Set(trainingContent.exams.map((exam) => exam.module))];
@@ -266,7 +183,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
<ToastContainer />
<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">
<span className="loading loading-infinity w-32 bg-mti-green-light" />
{isNewContentLoading && (
@@ -275,68 +192,7 @@ const Training: React.FC<{user: User}> = ({user}) => {
</div>
) : (
<>
<div className="w-full flex -xl:flex-col -xl:gap-4 justify-between items-center">
<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,
}),
}}
/>
</>
)}
<RecordFilter user={user} filterState={{ filter: filter, setFilter: setFilter }} assignments={false} >
{user.type === "student" && (
<>
<div className="flex items-center">
@@ -352,43 +208,13 @@ const Training: React.FC<{user: User}> = ({user}) => {
</div>
</>
)}
</div>
<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>
</RecordFilter>
{trainingContent.length == 0 && (
<div className="flex flex-grow justify-center items-center">
<span className="font-semibold ml-1">No training content to display...</span>
</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">
{Object.keys(filterTrainingContentByDate(groupedByTrainingContent))
.sort((a, b) => parseInt(b) - parseInt(a))