import React from 'react'; import { BsClock, BsXCircle } from 'react-icons/bs'; import clsx from 'clsx'; 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 { 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'; const formatTimestamp = (timestamp: string | number) => { const time = typeof timestamp === "string" ? parseInt(timestamp) : timestamp; const date = moment(time); const formatter = "YYYY/MM/DD - HH:mm"; return date.format(formatter); }; const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number; missing: number; correct: number }[] => { const scores: { [key in Module]: { total: number; missing: number; correct: number }; } = { reading: { total: 0, correct: 0, missing: 0, }, listening: { total: 0, correct: 0, missing: 0, }, writing: { total: 0, correct: 0, missing: 0, }, speaking: { total: 0, correct: 0, missing: 0, }, level: { total: 0, correct: 0, missing: 0, }, }; stats.forEach((x) => { scores[x.module!] = { total: scores[x.module!].total + x.score.total, correct: scores[x.module!].correct + x.score.correct, missing: scores[x.module!].missing + x.score.missing, }; }); return Object.keys(scores) .filter((x) => scores[x as Module].total > 0) .map((x) => ({ module: x as Module, ...scores[x as Module] })); }; interface StatsGridItemProps { width?: string | undefined; height?: string | undefined; examNumber?: number | undefined; stats: Stat[]; timestamp: string | number; user: User, assignments: Assignment[]; users: User[]; training?: boolean, selectedTrainingExams?: string[]; maxTrainingExams?: number; setSelectedTrainingExams?: React.Dispatch>; setExams: (exams: Exam[]) => void; setShowSolutions: (show: boolean) => void; setUserSolutions: (solutions: UserSolution[]) => void; setSelectedModules: (modules: Module[]) => void; setInactivity: (inactivity: number) => void; setTimeSpent: (time: number) => void; renderPdfIcon: (session: string, color: string, textColor: string) => React.ReactNode; } const StatsGridItem: React.FC = ({ stats, timestamp, user, assignments, users, training, selectedTrainingExams, setSelectedTrainingExams, setExams, setShowSolutions, setUserSolutions, setSelectedModules, setInactivity, setTimeSpent, renderPdfIcon, width = undefined, height = undefined, examNumber = undefined, maxTrainingExams = undefined }) => { const router = useRouter(); const correct = stats.reduce((accumulator, current) => accumulator + current.score.correct, 0); const total = stats.reduce((accumulator, current) => accumulator + current.score.total, 0); const aggregatedScores = aggregateScoresByModule(stats).filter((x) => x.total > 0); const assignmentID = stats.reduce((_, current) => current.assignment as any, ""); const assignment = assignments.find((a) => a.id === assignmentID); const isDisabled = stats.some((x) => x.isDisabled); const aiUsage = Math.round(ai_usage(stats) * 100); const aggregatedLevels = aggregatedScores.map((x) => ({ module: x.module, level: calculateBandScore(x.correct, x.total, x.module, user.focus), })); const textColor = clsx( correct / total >= 0.7 && "text-mti-purple", correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", correct / total < 0.3 && "text-mti-rose", ); const { timeSpent, inactivity, session } = stats[0]; const selectExam = () => { if (training && !isDisabled && typeof maxTrainingExams !== "undefined" && typeof setSelectedTrainingExams !== "undefined" && typeof timestamp == "string") { setSelectedTrainingExams(prevExams => { const uniqueExams = [...new Set(stats.map(stat => `${stat.module}-${stat.date}`))]; const indexes = uniqueExams.map(exam => prevExams.indexOf(exam)).filter(index => index !== -1); if (indexes.length > 0) { const newExams = [...prevExams]; indexes.sort((a, b) => b - a).forEach(index => { newExams.splice(index, 1); }); return newExams; } else { if (prevExams.length + uniqueExams.length <= maxTrainingExams) { return [...prevExams, ...uniqueExams]; } else { return prevExams; } } }); } else { const examPromises = uniqBy(stats, "exam").map((stat) => { return getExamById(stat.module, stat.exam); }); if (isDisabled) return; Promise.all(examPromises).then((exams) => { if (exams.every((x) => !!x)) { if (!!timeSpent) setTimeSpent(timeSpent); if (!!inactivity) setInactivity(inactivity); setUserSolutions(convertToUserSolutions(stats)); setShowSolutions(true); setExams(exams.map((x) => x!).sort(sortByModule)); setSelectedModules( exams .map((x) => x!) .sort(sortByModule) .map((x) => x!.module), ); router.push("/exercises"); } }); } }; const shouldRenderPDFIcon = () => { if(assignment) { return assignment.released; } return true; } const content = ( <>
{formatTimestamp(timestamp)}
{!!timeSpent && ( {Math.floor(timeSpent / 60)} minutes )} {!!inactivity && ( {Math.floor(inactivity / 60)} minutes )}
Level{" "} {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} {shouldRenderPDFIcon() && renderPdfIcon(session, textColor, textColor)}
{examNumber === undefined ? ( <> {aiUsage >= 50 && user.type !== "student" && (
= 80, } )}> AI Usage
)} ) : (
{examNumber}
)}
{aggregatedLevels.map(({ module, level }) => ( ))}
{assignment && ( Assignment: {assignment.name}, Teacher: {users.find((u) => u.id === assignment.assigner)?.name} )}
); return ( <>
= 0.7 && "hover:border-mti-purple", correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total < 0.3 && "hover:border-mti-rose", typeof selectedTrainingExams !== "undefined" && typeof timestamp === "string" && selectedTrainingExams.some(exam => exam.includes(timestamp)) && "border-2 border-slate-600", )} onClick={examNumber === undefined ? selectExam : undefined} style={{ ...(width !== undefined && { width }), ...(height !== undefined && { height }), }} data-tip="This exam is still being evaluated..." role="button"> {content}
= 0.7 && "hover:border-mti-purple", correct / total >= 0.3 && correct / total < 0.7 && "hover:border-mti-red", correct / total < 0.3 && "hover:border-mti-rose", )} data-tip="Your screen size is too small to view previous exams." style={{ ...(width !== undefined && { width }), ...(height !== undefined && { height }), }} role="button"> {content}
); }; export default StatsGridItem;