Files
encoach_frontend/src/pages/stats.tsx
2023-11-20 23:46:43 +00:00

347 lines
12 KiB
TypeScript

/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import {BsFileEarmarkText, BsPencil, BsStar} from "react-icons/bs";
import {LinearScale, Chart as ChartJS, CategoryScale, PointElement, LineElement, Legend, Tooltip, LineController} from "chart.js";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {useEffect, useState} from "react";
import useStats from "@/hooks/useStats";
import {averageScore, totalExamsByModule, groupBySession, groupByModule} from "@/utils/stats";
import useUser from "@/hooks/useUser";
import {ToastContainer} from "react-toastify";
import {capitalize} from "lodash";
import {Module} from "@/interfaces";
import ProgressBar from "@/components/Low/ProgressBar";
import Layout from "@/components/High/Layout";
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {Chart} from "react-chartjs-2";
import useUsers from "@/hooks/useUsers";
import Select from "react-select";
import useGroups from "@/hooks/useGroups";
import DatePicker from "react-datepicker";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import ProfileSummary from "@/components/ProfileSummary";
ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineController, Legend, Tooltip);
const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8"];
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
if (!user || !user.isVerified) {
res.setHeader("location", "/login");
res.statusCode = 302;
res.end();
return {
props: {
user: null,
},
};
}
if (shouldRedirectHome(user)) {
res.setHeader("location", "/");
res.statusCode = 302;
res.end();
return {
props: {
user: null,
},
};
}
return {
props: {user: req.session.user},
};
}, sessionOptions);
export default function Stats() {
const [statsUserId, setStatsUserId] = useState<string>();
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(new Date());
const {user} = useUser({redirectTo: "/login"});
const {users} = useUsers();
const {groups} = useGroups(user?.id);
const {stats} = useStats(statsUserId);
const {stats: userStats} = useStats(user?.id);
useEffect(() => {
if (user) setStatsUserId(user.id);
}, [user]);
// useEffect(() => {
// if (stats && stats.length > 0) {
// const sortedStats = stats.sort((a, b) => a.date - b.date);
// const firstStat = sortedStats.shift()!;
// setStartDate(moment.unix(firstStat.date).toDate());
// console.log(stats.filter((x) => moment.unix(x.date).isAfter(startDate)));
// console.log(stats.filter((x) => moment.unix(x.date).isBefore(endDate)));
// }
// }, [stats]);
const calculateTotalScorePerSession = () => {
const groupedBySession = groupBySession(stats);
const sessionAverage = Object.keys(groupedBySession).map((x: string) => {
const session = groupedBySession[x];
const moduleStats = groupByModule(session);
const moduleScores = Object.keys(moduleStats).map((y) => {
const correct = moduleStats[y].reduce((accumulator, current) => accumulator + current.score.correct, 0);
const total = moduleStats[y].reduce((accumulator, current) => accumulator + current.score.total, 0);
return {
module: y,
score: calculateBandScore(correct, total, y as Module, user?.focus || "academic"),
};
});
return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4;
});
return sessionAverage;
};
const calculateAverageTimePerModule = () => {
const groupedBySession = groupBySession(stats.filter((x) => !!x.timeSpent));
const sessionAverage = Object.keys(groupedBySession).map((x: string) => {
const session = groupedBySession[x];
const timeSpent = session[0].timeSpent!;
return Math.floor(timeSpent / session.length / 60);
});
return sessionAverage;
};
const calculateModularScorePerSession = (module: Module) => {
const groupedBySession = groupBySession(stats);
const sessionAverage = Object.keys(groupedBySession).map((x: string) => {
const session = groupedBySession[x];
const moduleStats = groupByModule(session);
if (!Object.keys(moduleStats).includes(module)) return null;
const correct = moduleStats[module].reduce((acc, curr) => acc + curr.score.correct, 0);
const total = moduleStats[module].reduce((acc, curr) => acc + curr.score.total, 0);
return calculateBandScore(correct, total, module, user?.focus || "academic");
});
return sessionAverage;
};
return (
<>
<Head>
<title>Stats | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<Layout user={user} className="gap-8">
<ProfileSummary
user={user}
items={[
{
icon: <BsFileEarmarkText className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: Object.keys(groupBySession(userStats)).length,
label: "Exams",
},
{
icon: <BsPencil className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: userStats.length,
label: "Exercises",
},
{
icon: <BsStar className="w-6 h-6 md:w-8 md:h-8 text-mti-red-light" />,
value: `${stats.length > 0 ? averageScore(userStats) : 0}%`,
label: "Average Score",
},
]}
/>
<section className="flex flex-col gap-3">
<div className="w-full flex justify-between gap-8 items-center">
<>
{(user.type === "developer" || user.type === "admin") && (
<Select
className="w-full"
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
onChange={(value) => setStatsUserId(value?.value)}
styles={{
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 && (
<Select
className="w-full"
options={users
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
onChange={(value) => setStatsUserId(value?.value)}
styles={{
option: (styles, state) => ({
...styles,
backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white",
color: state.isFocused ? "black" : styles.color,
}),
}}
/>
)}
</>
{/* <DatePicker
dateFormat="dd/MM/yyyy"
startDate={startDate}
endDate={endDate}
selectsRange
filterDate={(date) => !moment(date).isSameOrBefore(moment(startDate))}
onChange={([initialDate, finalDate]) => {
setStartDate(initialDate);
setEndDate(finalDate);
}}
/> */}
</div>
{stats.length > 0 && (
<div className="flex -md:flex-col -md:items-center gap-4 flex-wrap">
{/* Exams per module */}
<div className="flex flex-col gap-10 border w-full h-fit md:h-96 md:max-w-xs border-mti-gray-platinum p-4 pb-12 rounded-xl">
<span className="text-sm font-bold">Exams per Module</span>
<div className="flex flex-col gap-4">
{MODULE_ARRAY.map((module) => (
<div className="flex flex-col gap-2" key={module}>
<div className="flex justify-between items-end">
<span className="text-xs">
<span className="font-medium">{totalExamsByModule(stats, module)}</span> of{" "}
<span className="font-medium">{Object.keys(groupBySession(stats)).length}</span>
</span>
<span className="text-xs">{capitalize(module)}</span>
</div>
<ProgressBar
color={module}
percentage={(totalExamsByModule(stats, module) * 100) / Object.keys(groupBySession(stats)).length}
label=""
className="h-3"
/>
</div>
))}
</div>
</div>
{/* Module Score */}
<div className="flex flex-col gap-10 border w-full h-fit md:h-96 md:max-w-xs border-mti-gray-platinum p-4 pb-12 rounded-xl">
<span className="text-sm font-bold">Module Score Bands</span>
<div className="flex flex-col gap-4">
{MODULE_ARRAY.map((module) => (
<div className="flex flex-col gap-2" key={module}>
<div className="flex justify-between items-end">
<span className="text-xs">
<span className="font-medium">{user.levels[module]}</span> of{" "}
<span className="font-medium">{user.desiredLevels[module]}</span>
</span>
<span className="text-xs">{capitalize(module)}</span>
</div>
<ProgressBar
color={module}
percentage={(user.levels[module] * 100) / user.desiredLevels[module]}
label=""
className="h-3"
/>
</div>
))}
</div>
</div>
{/* Total Score Band per Session */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Total Score Band per Session</span>
<Chart
type="line"
data={{
labels: Object.keys(groupBySession(stats)).map((_, index) => index),
datasets: [
{
type: "line",
label: "Total",
fill: false,
borderColor: "#6A5FB1",
backgroundColor: "#7872BF",
borderWidth: 2,
spanGaps: true,
data: calculateTotalScorePerSession(),
},
],
}}
/>
</div>
{/* Module Score Band per Session */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Module Score Band per Session</span>
<Chart
type="line"
data={{
labels: Object.keys(groupBySession(stats)).map((_, index) => index),
datasets: [
...MODULE_ARRAY.map((module, index) => ({
type: "line" as const,
label: capitalize(module),
borderColor: COLORS[index],
backgroundColor: COLORS[index],
borderWidth: 2,
data: calculateModularScorePerSession(module),
})),
],
}}
/>
</div>
{/* Average Time per Module */}
<div className="w-full md:max-w-2xl border border-mti-gray-platinum p-4 pb-12 rounded-xl h-fit md:h-96">
<span className="text-sm font-bold">Average Time per Module (in Minutes)</span>
<Chart
type="line"
data={{
labels: Object.keys(groupBySession(stats.filter((s) => !!s.timeSpent))).map((_, index) => index),
datasets: [
{
type: "line",
label: "Average (in minutes)",
fill: false,
borderColor: "#6A5FB1",
backgroundColor: "#7872BF",
borderWidth: 2,
spanGaps: true,
data: calculateAverageTimePerModule(),
},
],
}}
/>
</div>
</div>
)}
</section>
{stats.length === 0 && (
<section className="flex flex-col gap-3">
<span className="font-semibold ml-1">No stats to display...</span>
</section>
)}
</Layout>
)}
</>
);
}