Major updates on Master Statistical

This commit is contained in:
Joao Ramos
2024-08-24 10:53:11 +01:00
parent 44adc142f6
commit cf1b47fbd2
7 changed files with 1397 additions and 878 deletions

View File

@@ -8,14 +8,23 @@ interface Props {
color: "purple" | "rose" | "red" | "green"; color: "purple" | "rose" | "red" | "green";
tooltip?: string; tooltip?: string;
onClick?: () => void; onClick?: () => void;
isSelected?: boolean;
} }
export default function IconCard({Icon, label, value, color, tooltip, onClick}: Props) { export default function IconCard({
Icon,
label,
value,
color,
tooltip,
onClick,
isSelected,
}: Props) {
const colorClasses: { [key in typeof color]: string } = { const colorClasses: { [key in typeof color]: string } = {
purple: "text-mti-purple-light", purple: "mti-purple-light",
red: "text-mti-red-light", red: "mti-red-light",
rose: "text-mti-rose-light", rose: "mti-rose-light",
green: "text-mti-green-light", green: "mti-green-light",
}; };
return ( return (
@@ -24,12 +33,16 @@ export default function IconCard({Icon, label, value, color, tooltip, onClick}:
className={clsx( className={clsx(
"bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center text-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300", "bg-white rounded-xl shadow p-4 flex flex-col gap-4 items-center text-center w-52 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300",
tooltip && "tooltip tooltip-bottom", tooltip && "tooltip tooltip-bottom",
isSelected && `border border-solid border-${colorClasses[color]}`
)} )}
data-tip={tooltip}> data-tip={tooltip}
<Icon className={clsx("text-6xl", colorClasses[color])} /> >
<Icon className={clsx("text-6xl", `text-${colorClasses[color]}`)} />
<span className="flex flex-col gap-1 items-center text-xl"> <span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">{label}</span> <span className="text-lg">{label}</span>
<span className={clsx("font-semibold", colorClasses[color])}>{value}</span> <span className={clsx("font-semibold", `text-${colorClasses[color]}`)}>
{value}
</span>
</span> </span>
</div> </div>
); );

View File

@@ -2,11 +2,17 @@
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import useStats from "@/hooks/useStats"; import useStats from "@/hooks/useStats";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {CorporateUser, Group, MasterCorporateUser, Stat, User} from "@/interfaces/user"; import {
CorporateUser,
Group,
MasterCorporateUser,
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";
import {useEffect, useState} from "react"; import { useEffect, useState, useMemo } from "react";
import { import {
BsArrowLeft, BsArrowLeft,
BsClipboard2Data, BsClipboard2Data,
@@ -27,7 +33,11 @@ import {
import UserCard from "@/components/UserCard"; import UserCard from "@/components/UserCard";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import {averageLevelCalculator, calculateAverageLevel, calculateBandScore} from "@/utils/score"; import {
averageLevelCalculator,
calculateAverageLevel,
calculateBandScore,
} from "@/utils/score";
import { MODULE_ARRAY } from "@/utils/moduleUtils"; import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import { groupByExam } from "@/utils/stats"; import { groupByExam } from "@/utils/stats";
@@ -50,7 +60,11 @@ import Checkbox from "@/components/Low/Checkbox";
import { groupBy, uniq, uniqBy } from "lodash"; import { groupBy, uniq, uniqBy } from "lodash";
import Select from "@/components/Low/Select"; import Select from "@/components/Low/Select";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import MasterStatistical from "./MasterStatistical"; import MasterStatistical from "./MasterStatistical";
interface Props { interface Props {
@@ -58,29 +72,51 @@ interface Props {
} }
const activeFilter = (a: Assignment) => const activeFilter = (a: Assignment) =>
moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; moment(a.endDate).isAfter(moment()) &&
const pastFilter = (a: Assignment) => (moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length) && !a.archived; moment(a.startDate).isBefore(moment()) &&
a.assignees.length > a.results.length;
const pastFilter = (a: Assignment) =>
(moment(a.endDate).isBefore(moment()) ||
a.assignees.length === a.results.length) &&
!a.archived;
const archivedFilter = (a: Assignment) => a.archived; const archivedFilter = (a: Assignment) => a.archived;
const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment());
type StudentPerformanceItem = User & {corporate?: CorporateUser; group?: Group}; type StudentPerformanceItem = User & {
const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPerformanceItem[]; stats: Stat[]; users: User[]; groups: Group[]}) => { corporate?: CorporateUser;
group?: Group;
};
const StudentPerformanceList = ({
items,
stats,
users,
groups,
}: {
items: StudentPerformanceItem[];
stats: Stat[];
users: User[];
groups: Group[];
}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false); const [isShowingAmount, setIsShowingAmount] = useState(false);
const [availableCorporates] = useState( const [availableCorporates] = useState(
uniqBy( uniqBy(
items.map((x) => x.corporate), items.map((x) => x.corporate),
"id", "id"
), )
); );
const [availableGroups] = useState( const [availableGroups] = useState(
uniqBy( uniqBy(
items.map((x) => x.group), items.map((x) => x.group),
"id", "id"
), )
); );
const [selectedCorporate, setSelectedCorporate] = useState<CorporateUser | null | undefined>(null); const [selectedCorporate, setSelectedCorporate] = useState<
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(null); CorporateUser | null | undefined
>(null);
const [selectedGroup, setSelectedGroup] = useState<Group | null | undefined>(
null
);
const columnHelper = createColumnHelper<StudentPerformanceItem>(); const columnHelper = createColumnHelper<StudentPerformanceItem>();
@@ -103,7 +139,10 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
}), }),
columnHelper.accessor("corporate", { columnHelper.accessor("corporate", {
header: "Corporate", header: "Corporate",
cell: (info) => (!!info.getValue() ? getUserCompanyName(info.getValue() as User, users, groups) : "N/A"), cell: (info) =>
!!info.getValue()
? getUserCompanyName(info.getValue() as User, users, groups)
: "N/A",
}), }),
columnHelper.accessor("levels.reading", { columnHelper.accessor("levels.reading", {
header: "Reading", header: "Reading",
@@ -111,15 +150,30 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter((x) => x.module === "reading" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter((x) => x.module === "reading" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic", info.row.original.focus || "academic"
) || 0 ) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`, : `${
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "reading" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.listening", { columnHelper.accessor("levels.listening", {
header: "Listening", header: "Listening",
@@ -127,15 +181,31 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter((x) => x.module === "listening" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "listening" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter((x) => x.module === "listening" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "listening" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic", info.row.original.focus || "academic"
) || 0 ) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`, : `${
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "listening" &&
x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.writing", { columnHelper.accessor("levels.writing", {
header: "Writing", header: "Writing",
@@ -143,15 +213,30 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter((x) => x.module === "writing" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter((x) => x.module === "writing" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic", info.row.original.focus || "academic"
) || 0 ) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`, : `${
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "writing" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.speaking", { columnHelper.accessor("levels.speaking", {
header: "Speaking", header: "Speaking",
@@ -159,15 +244,30 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter((x) => x.module === "speaking" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter((x) => x.module === "speaking" && x.user === info.row.original.id) .filter(
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic", info.row.original.focus || "academic"
) || 0 ) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`, : `${
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "speaking" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels.level", { columnHelper.accessor("levels.level", {
header: "Level", header: "Level",
@@ -175,15 +275,28 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!isShowingAmount !isShowingAmount
? calculateBandScore( ? calculateBandScore(
stats stats
.filter((x) => x.module === "level" && x.user === info.row.original.id) .filter(
(x) => x.module === "level" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.correct, 0), .reduce((acc, curr) => acc + curr.score.correct, 0),
stats stats
.filter((x) => x.module === "level" && x.user === info.row.original.id) .filter(
(x) => x.module === "level" && x.user === info.row.original.id
)
.reduce((acc, curr) => acc + curr.score.total, 0), .reduce((acc, curr) => acc + curr.score.total, 0),
"level", "level",
info.row.original.focus || "academic", info.row.original.focus || "academic"
) || 0 ) || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`, : `${
Object.keys(
groupByExam(
stats.filter(
(x) =>
x.module === "level" && x.user === info.row.original.id
)
)
).length
} exams`,
}), }),
columnHelper.accessor("levels", { columnHelper.accessor("levels", {
id: "overall_level", id: "overall_level",
@@ -192,16 +305,24 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!isShowingAmount !isShowingAmount
? averageLevelCalculator( ? averageLevelCalculator(
users, users,
stats.filter((x) => x.user === info.row.original.id), stats.filter((x) => x.user === info.row.original.id)
).toFixed(1) ).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`, : `${
Object.keys(
groupByExam(
stats.filter((x) => x.user === info.row.original.id)
)
).length
} exams`,
}), }),
]; ];
const filterUsers = (data: StudentPerformanceItem[]) => { const filterUsers = (data: StudentPerformanceItem[]) => {
console.log(data, selectedCorporate); console.log(data, selectedCorporate);
const filterByCorporate = (item: StudentPerformanceItem) => item.corporate?.id === selectedCorporate?.id; const filterByCorporate = (item: StudentPerformanceItem) =>
const filterByGroup = (item: StudentPerformanceItem) => item.group?.id === selectedGroup?.id; item.corporate?.id === selectedCorporate?.id;
const filterByGroup = (item: StudentPerformanceItem) =>
item.group?.id === selectedGroup?.id;
const filters: ((item: StudentPerformanceItem) => boolean)[] = []; const filters: ((item: StudentPerformanceItem) => boolean)[] = [];
if (selectedCorporate !== null) filters.push(filterByCorporate); if (selectedCorporate !== null) filters.push(filterByCorporate);
@@ -228,7 +349,10 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
<Select <Select
options={availableCorporates.map((x) => ({ options={availableCorporates.map((x) => ({
value: x?.id || "N/A", value: x?.id || "N/A",
label: x?.corporateInformation?.companyInformation?.name || x?.name || "N/A", label:
x?.corporateInformation?.companyInformation?.name ||
x?.name ||
"N/A",
}))} }))}
isClearable isClearable
value={ value={
@@ -237,7 +361,8 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
: { : {
value: selectedCorporate?.id || "N/A", value: selectedCorporate?.id || "N/A",
label: label:
selectedCorporate?.corporateInformation?.companyInformation?.name || selectedCorporate?.corporateInformation
?.companyInformation?.name ||
selectedCorporate?.name || selectedCorporate?.name ||
"N/A", "N/A",
} }
@@ -247,7 +372,11 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
!value !value
? setSelectedCorporate(null) ? setSelectedCorporate(null)
: setSelectedCorporate( : setSelectedCorporate(
value.value === "N/A" ? undefined : availableCorporates.find((x) => x?.id === value.value), value.value === "N/A"
? undefined
: availableCorporates.find(
(x) => x?.id === value.value
)
) )
} }
/> />
@@ -269,7 +398,11 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
onChange={(value) => onChange={(value) =>
!value !value
? setSelectedGroup(null) ? setSelectedGroup(null)
: setSelectedGroup(value.value === "N/A" ? undefined : availableGroups.find((x) => x?.id === value.value)) : setSelectedGroup(
value.value === "N/A"
? undefined
: availableGroups.find((x) => x?.id === value.value)
)
} }
/> />
</div> </div>
@@ -282,13 +415,13 @@ const StudentPerformanceList = ({items, stats, users, groups}: {items: StudentPe
(a, b) => (a, b) =>
averageLevelCalculator( averageLevelCalculator(
users, users,
stats.filter((x) => x.user === b.id), stats.filter((x) => x.user === b.id)
) - ) -
averageLevelCalculator( averageLevelCalculator(
users, users,
stats.filter((x) => x.user === a.id), stats.filter((x) => x.user === a.id)
), )
), )
)} )}
columns={columns} columns={columns}
/> />
@@ -302,18 +435,30 @@ export default function MasterCorporateDashboard({user}: Props) {
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [selectedAssignment, setSelectedAssignment] = useState<Assignment>(); const [selectedAssignment, setSelectedAssignment] = useState<Assignment>();
const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false);
const [corporateAssignments, setCorporateAssignments] = useState<(Assignment & {corporate?: CorporateUser})[]>([]); const [corporateAssignments, setCorporateAssignments] = useState<
(Assignment & { corporate?: CorporateUser })[]
>([]);
const { stats } = useStats(); const { stats } = useStats();
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 });
const masterCorporateUserGroups = [...new Set(groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants))]; const masterCorporateUserGroups = [
...new Set(
groups.filter((u) => u.admin === user.id).flatMap((g) => g.participants)
),
];
const corporateUserGroups = [...new Set(groups.flatMap((g) => g.participants))]; const corporateUserGroups = [
...new Set(groups.flatMap((g) => g.participants)),
];
const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({corporate: user.id}); const {
assignments,
isLoading: isAssignmentsLoading,
reload: reloadAssignments,
} = useAssignments({ corporate: user.id });
const appendUserFilters = useFilterStore((state) => state.appendUserFilter); const appendUserFilters = useFilterStore((state) => state.appendUserFilter);
const router = useRouter(); const router = useRouter();
@@ -327,21 +472,33 @@ export default function MasterCorporateDashboard({user}: Props) {
assignments.filter(activeFilter).map((a) => ({ assignments.filter(activeFilter).map((a) => ({
...a, ...a,
corporate: !!users.find((x) => x.id === a.assigner) corporate: !!users.find((x) => x.id === a.assigner)
? getCorporateUser(users.find((x) => x.id === a.assigner)!, users, groups) ? getCorporateUser(
users.find((x) => x.id === a.assigner)!,
users,
groups
)
: undefined, : undefined,
})), }))
); );
}, [assignments, groups, users]); }, [assignments, groups, users]);
const studentFilter = (user: User) => user.type === "student" && corporateUserGroups.includes(user.id); const studentFilter = (user: User) =>
const teacherFilter = (user: User) => user.type === "teacher" && corporateUserGroups.includes(user.id); user.type === "student" && corporateUserGroups.includes(user.id);
const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); const teacherFilter = (user: User) =>
user.type === "teacher" && corporateUserGroups.includes(user.id);
const getStatsByStudent = (user: User) =>
stats.filter((s) => s.user === user.id);
const UserDisplay = (displayUser: User) => ( const UserDisplay = (displayUser: User) => (
<div <div
onClick={() => setSelectedUser(displayUser)} onClick={() => setSelectedUser(displayUser)}
className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"> className="flex w-full p-4 gap-4 items-center hover:bg-mti-purple-ultralight cursor-pointer transition ease-in-out duration-300"
<img src={displayUser.profilePicture} alt={displayUser.name} className="rounded-full w-10 h-10" /> >
<img
src={displayUser.profilePicture}
alt={displayUser.name}
className="rounded-full w-10 h-10"
/>
<div className="flex flex-col gap-1 items-start"> <div className="flex flex-col gap-1 items-start">
<span>{displayUser.name}</span> <span>{displayUser.name}</span>
<span className="text-sm opacity-75">{displayUser.email}</span> <span className="text-sm opacity-75">{displayUser.email}</span>
@@ -351,7 +508,10 @@ export default function MasterCorporateDashboard({user}: Props) {
const StudentsList = () => { const StudentsList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "student" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id)); x.type === "student" &&
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -361,7 +521,8 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -374,7 +535,10 @@ export default function MasterCorporateDashboard({user}: Props) {
const TeachersList = () => { const TeachersList = () => {
const filter = (x: User) => const filter = (x: User) =>
x.type === "teacher" && (!!selectedUser ? corporateUserGroups.includes(x.id) || false : corporateUserGroups.includes(x.id)); x.type === "teacher" &&
(!!selectedUser
? corporateUserGroups.includes(x.id) || false
: corporateUserGroups.includes(x.id));
return ( return (
<UserList <UserList
@@ -384,7 +548,8 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -396,7 +561,10 @@ export default function MasterCorporateDashboard({user}: Props) {
}; };
const corporateUserFilter = (x: User) => const corporateUserFilter = (x: User) =>
x.type === "corporate" && (!!selectedUser ? masterCorporateUserGroups.includes(x.id) || false : masterCorporateUserGroups.includes(x.id)); x.type === "corporate" &&
(!!selectedUser
? masterCorporateUserGroups.includes(x.id) || false
: masterCorporateUserGroups.includes(x.id));
const CorporateList = () => { const CorporateList = () => {
return ( return (
@@ -407,7 +575,8 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -424,7 +593,8 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -451,7 +621,11 @@ export default function MasterCorporateDashboard({user}: Props) {
const StudentPerformancePage = () => { const StudentPerformancePage = () => {
const students = users const students = users
.filter((x) => x.type === "student" && groups.flatMap((g) => g.participants).includes(x.id)) .filter(
(x) =>
x.type === "student" &&
groups.flatMap((g) => g.participants).includes(x.id)
)
.map((u) => ({ .map((u) => ({
...u, ...u,
group: groups.find((x) => x.participants.includes(u.id)), group: groups.find((x) => x.participants.includes(u.id)),
@@ -463,18 +637,30 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} /> <BsArrowRepeat
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div> </div>
</div> </div>
<StudentPerformanceList items={students} stats={stats} users={users} groups={groups} /> <StudentPerformanceList
items={students}
stats={stats}
users={users}
groups={groups}
/>
</> </>
); );
}; };
@@ -493,7 +679,9 @@ export default function MasterCorporateDashboard({user}: Props) {
/> />
<AssignmentCreator <AssignmentCreator
assignment={selectedAssignment} assignment={selectedAssignment}
groups={groups.filter((x) => x.admin === user.id || x.participants.includes(user.id))} groups={groups.filter(
(x) => x.admin === user.id || x.participants.includes(user.id)
)}
users={users.filter( users={users.filter(
(x) => (x) =>
x.type === "student" && x.type === "student" &&
@@ -502,7 +690,7 @@ export default function MasterCorporateDashboard({user}: Props) {
.filter((g) => g.admin === selectedUser.id) .filter((g) => g.admin === selectedUser.id)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id) || false .includes(x.id) || false
: groups.flatMap((g) => g.participants).includes(x.id)), : groups.flatMap((g) => g.participants).includes(x.id))
)} )}
assigner={user.id} assigner={user.id}
isCreating={isCreatingAssignment} isCreating={isCreatingAssignment}
@@ -515,49 +703,86 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="w-full flex justify-between items-center"> <div className="w-full flex justify-between items-center">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
<div <div
onClick={reloadAssignments} onClick={reloadAssignments}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<span>Reload</span> <span>Reload</span>
<BsArrowRepeat className={clsx("text-xl", isAssignmentsLoading && "animate-spin")} /> <BsArrowRepeat
className={clsx(
"text-xl",
isAssignmentsLoading && "animate-spin"
)}
/>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-lg font-bold">Active Assignments Status</span> <span className="text-lg font-bold">Active Assignments Status</span>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span> <span>
<b>Total:</b> {assignments.filter(activeFilter).reduce((acc, curr) => acc + curr.results.length, 0)}/ <b>Total:</b>{" "}
{assignments.filter(activeFilter).reduce((acc, curr) => curr.exams.length + acc, 0)} {assignments
.filter(activeFilter)
.reduce((acc, curr) => acc + curr.results.length, 0)}
/
{assignments
.filter(activeFilter)
.reduce((acc, curr) => curr.exams.length + acc, 0)}
</span> </span>
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => ( {Object.keys(
groupBy(corporateAssignments, (x) => x.corporate?.id)
).map((x) => (
<div key={x}> <div key={x}>
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span> <span className="font-semibold">
{getUserCompanyName(
users.find((u) => u.id === x)!,
users,
groups
)}
:{" "}
</span>
<span> <span>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/ {groupBy(corporateAssignments, (x) => x.corporate?.id)[
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)} x
].reduce((acc, curr) => curr.results.length + acc, 0)}
/
{groupBy(corporateAssignments, (x) => x.corporate?.id)[
x
].reduce((acc, curr) => curr.exams.length + acc, 0)}
</span> </span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Active Assignments ({assignments.filter(activeFilter).length})</h2> <h2 className="text-2xl font-semibold">
Active Assignments ({assignments.filter(activeFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(activeFilter).map((a) => ( {assignments.filter(activeFilter).map((a) => (
<AssignmentCard {...a} users={users} onClick={() => setSelectedAssignment(a)} key={a.id} /> <AssignmentCard
{...a}
users={users}
onClick={() => setSelectedAssignment(a)}
key={a.id}
/>
))} ))}
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Planned Assignments ({assignments.filter(futureFilter).length})</h2> <h2 className="text-2xl font-semibold">
Planned Assignments ({assignments.filter(futureFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<div <div
onClick={() => setIsCreatingAssignment(true)} onClick={() => setIsCreatingAssignment(true)}
className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"> className="w-[250px] h-[200px] flex flex-col gap-2 items-center justify-center bg-white hover:bg-mti-purple-ultralight text-mti-purple-light hover:text-mti-purple-dark border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300"
>
<BsPlus className="text-6xl" /> <BsPlus className="text-6xl" />
<span className="text-lg">New Assignment</span> <span className="text-lg">New Assignment</span>
</div> </div>
@@ -575,7 +800,9 @@ export default function MasterCorporateDashboard({user}: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Past Assignments ({assignments.filter(pastFilter).length})</h2> <h2 className="text-2xl font-semibold">
Past Assignments ({assignments.filter(pastFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(pastFilter).map((a) => ( {assignments.filter(pastFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -592,7 +819,9 @@ export default function MasterCorporateDashboard({user}: Props) {
</div> </div>
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold">Archived Assignments ({assignments.filter(archivedFilter).length})</h2> <h2 className="text-2xl font-semibold">
Archived Assignments ({assignments.filter(archivedFilter).length})
</h2>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{assignments.filter(archivedFilter).map((a) => ( {assignments.filter(archivedFilter).map((a) => (
<AssignmentCard <AssignmentCard
@@ -612,13 +841,24 @@ export default function MasterCorporateDashboard({user}: Props) {
); );
}; };
const masterCorporateUsers = useMemo(
() =>
masterCorporateUserGroups.reduce((accm: CorporateUser[], id) => {
const user = users.find((u) => u.id === id) as CorporateUser;
if (user) return [...accm, user];
return accm;
}, []),
[masterCorporateUserGroups, users]
);
const MasterStatisticalPage = () => { const MasterStatisticalPage = () => {
return ( return (
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div <div
onClick={() => setPage("")} onClick={() => setPage("")}
className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"> className="flex gap-2 items-center text-mti-purple-light cursor-pointer hover:text-mti-purple-dark transition ease-in-out duration-300"
>
<BsArrowLeft className="text-xl" /> <BsArrowLeft className="text-xl" />
<span>Back</span> <span>Back</span>
</div> </div>
@@ -626,11 +866,7 @@ export default function MasterCorporateDashboard({user}: Props) {
</div> </div>
<MasterStatistical <MasterStatistical
users={users} users={users}
corporateUsers={masterCorporateUserGroups.reduce((accm: CorporateUser[], id) => { corporateUsers={masterCorporateUsers}
const user = users.find((u) => u.id === id) as CorporateUser;
if (user) return [...accm, user];
return accm;
}, [])}
/> />
</> </>
); );
@@ -656,7 +892,11 @@ export default function MasterCorporateDashboard({user}: Props) {
<IconCard <IconCard
Icon={BsClipboard2Data} Icon={BsClipboard2Data}
label="Exams Performed" label="Exams Performed"
value={stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)).length} value={
stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
).length
}
color="purple" color="purple"
/> />
<IconCard <IconCard
@@ -664,21 +904,35 @@ export default function MasterCorporateDashboard({user}: Props) {
label="Average Level" label="Average Level"
value={averageLevelCalculator( value={averageLevelCalculator(
users, users,
stats.filter((s) => groups.flatMap((g) => g.participants).includes(s.user)), stats.filter((s) =>
groups.flatMap((g) => g.participants).includes(s.user)
)
).toFixed(1)} ).toFixed(1)}
color="purple" color="purple"
/> />
<IconCard onClick={() => setPage("groups")} Icon={BsPeople} label="Groups" value={groups.length} color="purple" /> <IconCard
onClick={() => setPage("groups")}
Icon={BsPeople}
label="Groups"
value={groups.length}
color="purple"
/>
<IconCard <IconCard
Icon={BsPersonCheck} Icon={BsPersonCheck}
label="User Balance" label="User Balance"
value={`${codes.length}/${user.corporateInformation?.companyInformation?.userAmount || 0}`} value={`${codes.length}/${
user.corporateInformation?.companyInformation?.userAmount || 0
}`}
color="purple" color="purple"
/> />
<IconCard <IconCard
Icon={BsClock} Icon={BsClock}
label="Expiration Date" label="Expiration Date"
value={user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy") : "Unlimited"} value={
user.subscriptionExpirationDate
? moment(user.subscriptionExpirationDate).format("DD/MM/yyyy")
: "Unlimited"
}
color="rose" color="rose"
/> />
<IconCard <IconCard
@@ -705,12 +959,15 @@ export default function MasterCorporateDashboard({user}: Props) {
<button <button
disabled={isAssignmentsLoading} disabled={isAssignmentsLoading}
onClick={() => setPage("assignments")} onClick={() => setPage("assignments")}
className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"> className="bg-white col-span-2 rounded-xl shadow p-4 flex flex-col gap-4 items-center w-96 h-52 justify-center cursor-pointer hover:shadow-xl transition ease-in-out duration-300"
>
<BsEnvelopePaper className="text-6xl text-mti-purple-light" /> <BsEnvelopePaper className="text-6xl text-mti-purple-light" />
<span className="flex flex-col gap-1 items-center text-xl"> <span className="flex flex-col gap-1 items-center text-xl">
<span className="text-lg">Assignments</span> <span className="text-lg">Assignments</span>
<span className="font-semibold text-mti-purple-light"> <span className="font-semibold text-mti-purple-light">
{isAssignmentsLoading ? "Loading..." : assignments.filter((a) => !a.archived).length} {isAssignmentsLoading
? "Loading..."
: assignments.filter((a) => !a.archived).length}
</span> </span>
</span> </span>
</button> </button>
@@ -744,7 +1001,11 @@ export default function MasterCorporateDashboard({user}: Props) {
<div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide"> <div className="flex flex-col items-start h-96 overflow-scroll scrollbar-hide">
{users {users
.filter(studentFilter) .filter(studentFilter)
.sort((a, b) => calculateAverageLevel(b.levels) - calculateAverageLevel(a.levels)) .sort(
(a, b) =>
calculateAverageLevel(b.levels) -
calculateAverageLevel(a.levels)
)
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
))} ))}
@@ -757,7 +1018,8 @@ export default function MasterCorporateDashboard({user}: Props) {
.filter(studentFilter) .filter(studentFilter)
.sort( .sort(
(a, b) => (a, b) =>
Object.keys(groupByExam(getStatsByStudent(b))).length - Object.keys(groupByExam(getStatsByStudent(a))).length, Object.keys(groupByExam(getStatsByStudent(b))).length -
Object.keys(groupByExam(getStatsByStudent(a))).length
) )
.map((x) => ( .map((x) => (
<UserDisplay key={x.id} {...x} /> <UserDisplay key={x.id} {...x} />
@@ -781,7 +1043,8 @@ export default function MasterCorporateDashboard({user}: Props) {
if (shouldReload) reload(); if (shouldReload) reload();
}} }}
onViewStudents={ onViewStudents={
selectedUser.type === "corporate" || selectedUser.type === "teacher" selectedUser.type === "corporate" ||
selectedUser.type === "teacher"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-students", id: "view-students",
@@ -791,7 +1054,11 @@ export default function MasterCorporateDashboard({user}: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) .filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });
@@ -801,7 +1068,8 @@ export default function MasterCorporateDashboard({user}: Props) {
: undefined : undefined
} }
onViewTeachers={ onViewTeachers={
selectedUser.type === "corporate" || selectedUser.type === "student" selectedUser.type === "corporate" ||
selectedUser.type === "student"
? () => { ? () => {
appendUserFilters({ appendUserFilters({
id: "view-teachers", id: "view-teachers",
@@ -811,7 +1079,11 @@ export default function MasterCorporateDashboard({user}: Props) {
id: "belongs-to-admin", id: "belongs-to-admin",
filter: (x: User) => filter: (x: User) =>
groups groups
.filter((g) => g.admin === selectedUser.id || g.participants.includes(selectedUser.id)) .filter(
(g) =>
g.admin === selectedUser.id ||
g.participants.includes(selectedUser.id)
)
.flatMap((g) => g.participants) .flatMap((g) => g.participants)
.includes(x.id), .includes(x.id),
}); });

View File

@@ -5,11 +5,20 @@ import IconCard from "./IconCard";
import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates"; import useAssignmentsCorporates from "@/hooks/useAssignmentCorporates";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
import moment from "moment"; import moment from "moment";
import { groupBySession } from "@/utils/stats"; import { Assignment, AssignmentWithCorporateId } from "@/interfaces/results";
import { Assignment, AssignmentResult } from "@/interfaces/results"; import {
CellContext,
createColumnHelper,
flexRender,
getCoreRowModel,
HeaderGroup,
Table,
useReactTable,
} from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox";
interface Props { interface Props {
corporateUsers: CorporateUser[]; corporateUsers: User[];
users: User[]; users: User[];
} }
@@ -20,11 +29,29 @@ interface TableData {
submitted: boolean; submitted: boolean;
date: moment.Moment; date: moment.Moment;
assignment: string; assignment: string;
corporateId: string;
} }
interface UserCount {
userCount: number;
maxUserCount: number;
}
const MasterStatistical = (props: Props) => { const MasterStatistical = (props: Props) => {
const { users, corporateUsers } = props; const { users, corporateUsers } = props;
const corporates = React.useMemo(() => corporateUsers.map((x) => x.id), [corporateUsers]); const corporateRelevantUsers = React.useMemo(
() => corporateUsers.filter((x) => x.type !== "student") as CorporateUser[],
[corporateUsers]
);
const corporates = React.useMemo(
() => corporateRelevantUsers.map((x) => x.id),
[corporateRelevantUsers]
);
const [selectedCorporates, setSelectedCorporates] =
React.useState<string[]>(corporates);
const [startDate, setStartDate] = React.useState<Date | null>( const [startDate, setStartDate] = React.useState<Date | null>(
moment("01/01/2023").toDate() moment("01/01/2023").toDate()
); );
@@ -33,51 +60,189 @@ const MasterStatistical = (props: Props) => {
); );
const { assignments } = useAssignmentsCorporates({ const { assignments } = useAssignmentsCorporates({
corporates, // corporates: [...corporates, "tYU0HTiJdjMsS8SB7XJsUdMMP892"],
corporates: selectedCorporates,
startDate, startDate,
endDate, endDate,
}); });
const tableResults = React.useMemo(
const x = assignments.reduce((accmA: TableData[], a: Assignment) => { () =>
const userResults = a.results.reduce((accmB: TableData[], r: AssignmentResult) => { assignments.reduce((accmA: TableData[], a: AssignmentWithCorporateId) => {
const userStats = groupBySession(r.stats); const userResults = a.assignees.map((assignee) => {
const data = Object.keys(userStats).map((key) => ({ const userStats =
user: users.find((u) => u.id === r.user)?.name || "", a.results.find((r) => r.user === assignee)?.stats || [];
correct: userStats[key].reduce((n, e) => n + e.score.correct, 0), const userName = users.find((u) => u.id === assignee)?.name || "";
corporate: users.find((u) => u.id === a.assigner)?.name || "", const corporate = users.find((u) => u.id === a.assigner)?.name || "";
submitted: false, const commonData = {
date: moment.max(userStats[key].map((e) => moment(e.date))), user: userName,
corporateId: a.corporateId,
corporate,
assignment: a.name, assignment: a.name,
})); };
return [...accmB, ...data]; if (userStats.length === 0) {
}, []); return {
...commonData,
correct: 0,
submitted: false,
// date: moment(),
};
}
return {
...commonData,
correct: userStats.reduce((n, e) => n + e.score.correct, 0),
submitted: true,
date: moment.max(userStats.map((e) => moment(e.date))),
};
}) as TableData[];
return [...accmA, ...userResults]; return [...accmA, ...userResults];
}, []); }, []),
[assignments, users]
);
const getCorporateScores = (corporateId: string): UserCount => {
const corporateAssignmentsUsers = assignments
.filter((a) => a.corporateId === corporateId)
.reduce((acc, a) => acc + a.assignees.length, 0);
const corporateResults = tableResults.filter(
(r) => r.corporateId === corporateId
).length;
return {
maxUserCount: corporateAssignmentsUsers,
userCount: corporateResults,
};
};
const corporateScores = corporates.reduce(
(accm, id) => ({
...accm,
[id]: getCorporateScores(id),
}),
{}
) as Record<string, UserCount>;
const consolidateScore = Object.values(corporateScores).reduce(
(acc: UserCount, { userCount, maxUserCount }: UserCount) => ({
userCount: acc.userCount + userCount,
maxUserCount: acc.maxUserCount + maxUserCount,
}),
{ userCount: 0, maxUserCount: 0 }
);
const getConsolidateScoreStr = (data: UserCount) =>
`${data.userCount}/${data.maxUserCount}`;
const columnHelper = createColumnHelper<TableData>();
const defaultColumns = [
columnHelper.accessor("user", {
header: "User",
id: "user",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("corporate", {
header: "Corporate",
id: "corporate",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("assignment", {
header: "Assignment",
id: "assignment",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("submitted", {
header: "Submitted",
id: "submitted",
cell: (info) => {
return ( return (
<Checkbox isChecked={info.getValue()} disabled onChange={() => {}}>
<span></span>
</Checkbox>
);
},
}),
columnHelper.accessor("correct", {
header: "Correct",
id: "correct",
cell: (info) => {
return <span>{info.getValue()}</span>;
},
}),
columnHelper.accessor("date", {
header: "Date",
id: "date",
cell: (info) => {
const date = info.getValue();
if (date) {
return <span>{date.format("DD/MM/YYYY")}</span>;
}
return <span>{""}</span>;
},
}),
];
const table = useReactTable({
data: tableResults,
columns: defaultColumns,
getCoreRowModel: getCoreRowModel(),
});
const areAllSelected = selectedCorporates.length === corporates.length;
return (
<>
<div className="flex flex-wrap gap-2 items-center text-center"> <div className="flex flex-wrap gap-2 items-center text-center">
<IconCard <IconCard
Icon={BsBank} Icon={BsBank}
label="Consolidate" label="Consolidate"
value={0} value={getConsolidateScoreStr(consolidateScore)}
color="purple" color="purple"
onClick={() => console.log("clicked")} onClick={() => {
if (areAllSelected) {
setSelectedCorporates([]);
return;
}
setSelectedCorporates(corporates);
}}
isSelected={areAllSelected}
/> />
{corporateUsers.map((group) => ( {corporateRelevantUsers.map((group) => {
const isSelected = selectedCorporates.includes(group.id);
return (
<IconCard <IconCard
key={group.id} key={group.id}
Icon={BsBank} Icon={BsBank}
label={group.corporateInformation?.companyInformation?.name} label={group.corporateInformation?.companyInformation?.name}
value={0} value={getConsolidateScoreStr(corporateScores[group.id])}
color="purple" color="purple"
onClick={() => console.log("clicked", group)} onClick={() => {
if (isSelected) {
setSelectedCorporates(
selectedCorporates.filter((x) => x !== group.id)
);
return;
}
setSelectedCorporates([...selectedCorporates, group.id]);
}}
isSelected={isSelected}
/> />
))} );
})}
</div>
<div className="flex gap-3 w-full">
<ReactDatePicker <ReactDatePicker
dateFormat="dd/MM/yyyy" dateFormat="dd/MM/yyyy"
className="px-4 py-6 w-full text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none" className="px-4 py-6 w-52 text-sm text-center font-normal placeholder:text-mti-gray-cool disabled:bg-mti-gray-platinum/40 disabled:text-mti-gray-dim disabled:cursor-not-allowed rounded-full border border-mti-gray-platinum focus:outline-none"
selected={startDate} selected={startDate}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
@@ -94,7 +259,42 @@ const MasterStatistical = (props: Props) => {
setEndDate(null); setEndDate(null);
}} }}
/> />
</div>
<div>
<table className="rounded-xl h-full bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="p-4 text-left" key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
key={row.id}
>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div>
<IconCard <IconCard
onClick={() => console.log("clicked")} onClick={() => console.log("clicked")}
Icon={BsPersonFill} Icon={BsPersonFill}
@@ -102,6 +302,7 @@ const MasterStatistical = (props: Props) => {
color="purple" color="purple"
/> />
</div> </div>
</>
); );
}; };

View File

@@ -1,6 +1,5 @@
import { Assignment } from "@/interfaces/results"; import { AssignmentWithCorporateId } from "@/interfaces/results";
import axios from "axios"; import axios from "axios";
import moment from "moment";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function useAssignmentsCorporates({ export default function useAssignmentsCorporates({
@@ -12,7 +11,7 @@ export default function useAssignmentsCorporates({
startDate: Date | null; startDate: Date | null;
endDate: Date | null; endDate: Date | null;
}) { }) {
const [assignments, setAssignments] = useState<Assignment[]>([]); const [assignments, setAssignments] = useState<AssignmentWithCorporateId[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
@@ -30,7 +29,7 @@ export default function useAssignmentsCorporates({
}); });
axios axios
.get<Assignment[]>( .get<AssignmentWithCorporateId[]>(
`/api/assignments/corporate?${urlSearchParams.toString()}` `/api/assignments/corporate?${urlSearchParams.toString()}`
) )
.then(async (response) => { .then(async (response) => {

View File

@@ -29,3 +29,5 @@ export interface Assignment {
archived?: boolean; archived?: boolean;
released?: boolean; released?: boolean;
} }
export type AssignmentWithCorporateId = Assignment & { corporateId: string };

View File

@@ -5,7 +5,6 @@ import { sessionOptions } from "@/lib/session";
import { getAllAssignersByCorporate } from "@/utils/groups.be"; import { getAllAssignersByCorporate } from "@/utils/groups.be";
import { getAssignmentsByAssigners } from "@/utils/assignments.be"; import { getAssignmentsByAssigners } from "@/utils/assignments.be";
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -20,17 +19,49 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} }
async function GET(req: NextApiRequest, res: NextApiResponse) { async function GET(req: NextApiRequest, res: NextApiResponse) {
const { ids, startDate, endDate } = req.query as { ids: string, startDate?: string, endDate?: string }; const { ids, startDate, endDate } = req.query as {
ids: string;
startDate?: string;
endDate?: string;
};
const startDateParsed = startDate ? new Date(startDate) : undefined; const startDateParsed = startDate ? new Date(startDate) : undefined;
const endDateParsed = endDate ? new Date(endDate) : undefined; const endDateParsed = endDate ? new Date(endDate) : undefined;
try { try {
const idsList = ids.split(","); const idsList = ids.split(",");
const assigners = await Promise.all(idsList.map(getAllAssignersByCorporate)); const assigners = await Promise.all(
const assignmentList = [...assigners.flat(), ...idsList]; idsList.map(async (id) => {
const assignments = await getAssignmentsByAssigners(assignmentList, startDateParsed, endDateParsed); const assigners = await getAllAssignersByCorporate(id);
res.status(200).json(assignments); return {
corporateId: id,
assigners,
};
})
);
const assignments = await Promise.all(assigners.map(async (data) => {
try {
const assigners = [...new Set([...data.assigners, data.corporateId])];
const assignments = await getAssignmentsByAssigners(
assigners,
startDateParsed,
endDateParsed
);
return assignments.map((assignment) => ({
...assignment,
corporateId: data.corporateId,
}));
} catch (err) {
console.error(err);
return [];
}
}));
console.log(assignments);
// const assignments = await getAssignmentsByAssigners(assignmentList, startDateParsed, endDateParsed);
res.status(200).json(assignments.flat());
} catch (err: any) { } catch (err: any) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }

View File

@@ -128,6 +128,7 @@ export const groupBySession = (stats: Stat[]) => groupBy(stats, "session");
export const groupByDate = (stats: Stat[]) => groupBy(stats, "date"); export const groupByDate = (stats: Stat[]) => groupBy(stats, "date");
export const groupByExam = (stats: Stat[]) => groupBy(stats, "exam"); export const groupByExam = (stats: Stat[]) => groupBy(stats, "exam");
export const groupByModule = (stats: Stat[]) => groupBy(stats, "module"); export const groupByModule = (stats: Stat[]) => groupBy(stats, "module");
export const groupByUser = (stats: Stat[]) => groupBy(stats, "user");
export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => { export const convertToUserSolutions = (stats: Stat[]): UserSolution[] => {
return stats.map((stat) => ({ return stats.map((stat) => ({