ENCOA-269
This commit is contained in:
@@ -3,7 +3,7 @@ import { RolePermission } from "@/resources/entityPermissions";
|
|||||||
export interface Entity {
|
export interface Entity {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
licenses: number
|
licenses: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
/* 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 {BsArrowClockwise, BsChevronLeft, BsChevronRight, BsFileEarmarkText, BsPencil, BsStar} from "react-icons/bs";
|
import { BsArrowClockwise, BsChevronLeft, BsChevronRight, BsFileEarmarkText, BsPencil, BsStar } from "react-icons/bs";
|
||||||
import {LinearScale, Chart as ChartJS, CategoryScale, PointElement, LineElement, Legend, Tooltip, LineController} from "chart.js";
|
import { LinearScale, Chart as ChartJS, CategoryScale, PointElement, LineElement, Legend, Tooltip, LineController } from "chart.js";
|
||||||
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, useMemo, useState } from "react";
|
||||||
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
|
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";
|
||||||
import {capitalize, Dictionary} from "lodash";
|
import { capitalize, Dictionary } from "lodash";
|
||||||
import {Module} from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
import ProgressBar from "@/components/Low/ProgressBar";
|
||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import {calculateAverageLevel, calculateBandScore} from "@/utils/score";
|
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||||
import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils";
|
import { countExamModules, countFullExams, MODULE_ARRAY, sortByModule } from "@/utils/moduleUtils";
|
||||||
import {Chart} from "react-chartjs-2";
|
import { Chart } from "react-chartjs-2";
|
||||||
import useUsers from "@/hooks/useUsers";
|
import useUsers from "@/hooks/useUsers";
|
||||||
import useGroups from "@/hooks/useGroups";
|
import useGroups from "@/hooks/useGroups";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
import { shouldRedirectHome } from "@/utils/navigation.disabled";
|
||||||
import ProfileSummary from "@/components/ProfileSummary";
|
import ProfileSummary from "@/components/ProfileSummary";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {Group, Stat, User} from "@/interfaces/user";
|
import { Group, Stat, User } from "@/interfaces/user";
|
||||||
import {Divider} from "primereact/divider";
|
import { Divider } from "primereact/divider";
|
||||||
import Badge from "@/components/Low/Badge";
|
import Badge from "@/components/Low/Badge";
|
||||||
import { mapBy, redirect, serialize } from "@/utils";
|
import { filterBy, mapBy, redirect, serialize } from "@/utils";
|
||||||
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
import { getEntitiesWithRoles } from "@/utils/entities.be";
|
||||||
import { checkAccess } from "@/utils/permissions";
|
import { checkAccess } from "@/utils/permissions";
|
||||||
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
|
||||||
@@ -38,7 +38,7 @@ ChartJS.register(LinearScale, CategoryScale, PointElement, LineElement, LineCont
|
|||||||
|
|
||||||
const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"];
|
const COLORS = ["#1EB3FF", "#FF790A", "#3D9F11", "#EF5DA8", "#414288"];
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return redirect("/login")
|
if (!user) return redirect("/login")
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
|
|||||||
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
const groups = await (checkAccess(user, ["admin", "developer"]) ? getGroups() : getGroupsByEntities(mapBy(entities, 'id')))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({user, entities, users, groups}),
|
props: serialize({ user, entities, users, groups }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
const [startDate, setStartDate] = useState<Date | null>(moment(new Date()).subtract(1, "weeks").toDate());
|
const [startDate, setStartDate] = useState<Date | null>(moment(new Date()).subtract(1, "weeks").toDate());
|
||||||
const [endDate, setEndDate] = useState<Date | null>(new Date());
|
const [endDate, setEndDate] = useState<Date | null>(new Date());
|
||||||
const [initialStatDate, setInitialStatDate] = useState<Date>();
|
const [initialStatDate, setInitialStatDate] = useState<Date>();
|
||||||
|
const [selectedEntity, setSelectedEntity] = useState<string>()
|
||||||
|
|
||||||
const [monthlyOverallScoreDate, setMonthlyOverallScoreDate] = useState<Date | null>(new Date());
|
const [monthlyOverallScoreDate, setMonthlyOverallScoreDate] = useState<Date | null>(new Date());
|
||||||
const [monthlyModuleScoreDate, setMonthlyModuleScoreDate] = useState<Date | null>(new Date());
|
const [monthlyModuleScoreDate, setMonthlyModuleScoreDate] = useState<Date | null>(new Date());
|
||||||
@@ -73,7 +74,12 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
const [dailyScoreDate, setDailyScoreDate] = useState<Date | null>(new Date());
|
const [dailyScoreDate, setDailyScoreDate] = useState<Date | null>(new Date());
|
||||||
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
const [intervalDates, setIntervalDates] = useState<Date[]>([]);
|
||||||
|
|
||||||
const {data: stats} = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
|
const { data: stats } = useFilterRecordsByUser<Stat[]>(statsUserId, !statsUserId);
|
||||||
|
|
||||||
|
const students = useMemo(() =>
|
||||||
|
filterBy(users, 'type', 'student').filter(x => !selectedEntity ? true : mapBy(x.entities, 'id').includes((selectedEntity))),
|
||||||
|
[users, selectedEntity]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInitialStatDate(
|
setInitialStatDate(
|
||||||
@@ -181,26 +187,24 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
|
|
||||||
<section className="flex flex-col gap-3">
|
<section className="flex flex-col gap-3">
|
||||||
<div className="w-full flex justify-between gap-4 items-center">
|
<div className="w-full flex justify-between gap-4 items-center">
|
||||||
<>
|
{["corporate", "teacher", "mastercorporate", "developer", "admin"].includes(user.type) && (
|
||||||
{(user.type === "developer" || user.type === "admin") && (
|
<>
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
options={users.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
options={entities.map(e => ({ value: e.id, label: e.label }))}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
onChange={(value) => setSelectedEntity(value?.value || undefined)}
|
||||||
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
placeholder="Select an entity..."
|
||||||
|
isClearable
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{["corporate", "teacher", "mastercorporate"].includes(user.type) && groups.length > 0 && (
|
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
options={users
|
options={students
|
||||||
.filter((x) => groups.flatMap((y) => y.participants).includes(x.id))
|
.map((x) => ({ value: x.id, label: `${x.name} - ${x.email}` }))}
|
||||||
.map((x) => ({value: x.id, label: `${x.name} - ${x.email}`}))}
|
defaultValue={{ value: user.id, label: `${user.name} - ${user.email}` }}
|
||||||
defaultValue={{value: user.id, label: `${user.name} - ${user.email}`}}
|
|
||||||
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
onChange={(value) => setStatsUserId(value?.value || user.id)}
|
||||||
/>
|
/>
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.length > 0 && (
|
{stats.length > 0 && (
|
||||||
@@ -244,8 +248,7 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
<div className="w-full grid grid-cols-3 gap-4 items-center">
|
<div className="w-full grid grid-cols-3 gap-4 items-center">
|
||||||
{[...Array(31).keys()].map((day) => {
|
{[...Array(31).keys()].map((day) => {
|
||||||
const date = moment(
|
const date = moment(
|
||||||
`${(day + 1).toString().padStart(2, "0")}/${
|
`${(day + 1).toString().padStart(2, "0")}/${moment(monthlyOverallScoreDate).get("month") + 1
|
||||||
moment(monthlyOverallScoreDate).get("month") + 1
|
|
||||||
}/${moment(monthlyOverallScoreDate).get("year")}`,
|
}/${moment(monthlyOverallScoreDate).get("year")}`,
|
||||||
"DD/MM/yyyy",
|
"DD/MM/yyyy",
|
||||||
);
|
);
|
||||||
@@ -319,8 +322,7 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
labels: [...Array(31).keys()]
|
labels: [...Array(31).keys()]
|
||||||
.map((day) => {
|
.map((day) => {
|
||||||
const date = moment(
|
const date = moment(
|
||||||
`${(day + 1).toString().padStart(2, "0")}/${
|
`${(day + 1).toString().padStart(2, "0")}/${moment(monthlyOverallScoreDate).get("month") + 1
|
||||||
moment(monthlyOverallScoreDate).get("month") + 1
|
|
||||||
}/${moment(monthlyOverallScoreDate).get("year")}`,
|
}/${moment(monthlyOverallScoreDate).get("year")}`,
|
||||||
"DD/MM/yyyy",
|
"DD/MM/yyyy",
|
||||||
);
|
);
|
||||||
@@ -339,17 +341,16 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
data: [...Array(31).keys()]
|
data: [...Array(31).keys()]
|
||||||
.map((day) => {
|
.map((day) => {
|
||||||
const date = moment(
|
const date = moment(
|
||||||
`${(day + 1).toString().padStart(2, "0")}/${
|
`${(day + 1).toString().padStart(2, "0")}/${moment(monthlyOverallScoreDate).get("month") + 1
|
||||||
moment(monthlyOverallScoreDate).get("month") + 1
|
|
||||||
}/${moment(monthlyOverallScoreDate).get("year")}`,
|
}/${moment(monthlyOverallScoreDate).get("year")}`,
|
||||||
"DD/MM/yyyy",
|
"DD/MM/yyyy",
|
||||||
);
|
);
|
||||||
|
|
||||||
return date.isValid()
|
return date.isValid()
|
||||||
? calculateTotalScore(
|
? calculateTotalScore(
|
||||||
stats.filter((s) => timestampToMoment(s).isBefore(date)),
|
stats.filter((s) => timestampToMoment(s).isBefore(date)),
|
||||||
5,
|
5,
|
||||||
).toFixed(1)
|
).toFixed(1)
|
||||||
: undefined;
|
: undefined;
|
||||||
})
|
})
|
||||||
.filter((x) => !!x),
|
.filter((x) => !!x),
|
||||||
@@ -396,7 +397,7 @@ export default function Stats({ user, entities, users, groups }: Props) {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate))))
|
{calculateModuleScore(stats.filter((s) => timestampToMoment(s).isBefore(moment(monthlyModuleScoreDate))))
|
||||||
.sort(sortByModule)
|
.sort(sortByModule)
|
||||||
.map(({module, score}) => (
|
.map(({ module, score }) => (
|
||||||
<div className="flex flex-col gap-2" key={module}>
|
<div className="flex flex-col gap-2" key={module}>
|
||||||
<div className="flex justify-between items-end">
|
<div className="flex justify-between items-end">
|
||||||
<span className="text-xs">
|
<span className="text-xs">
|
||||||
|
|||||||
Reference in New Issue
Block a user