From 46764cacfa506e9c5013a83e7206272083a48a40 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 2 Apr 2024 00:25:49 +0100 Subject: [PATCH 1/3] Updated the stats --- src/components/UserCard.tsx | 2 +- src/dashboards/Student.tsx | 14 +++++++------- src/pages/stats.tsx | 18 +++++++++--------- src/utils/moduleUtils.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index cd5faaaf..1580272d 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -157,7 +157,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, { icon: , value: stats.length, - label: "Exercises", + label: "Modules", }, { icon: , diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index 40b376b0..3e5f5999 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -13,7 +13,7 @@ import {CorporateUser, User} from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; import {getExamById} from "@/utils/exams"; import {getUserCorporate} from "@/utils/groups"; -import {MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils"; +import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule, sortByModuleName} from "@/utils/moduleUtils"; import {getLevelLabel, getLevelScore} from "@/utils/score"; import {averageScore, groupBySession} from "@/utils/stats"; import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OrderResponseBody} from "@paypal/paypal-js"; @@ -84,16 +84,16 @@ export default function StudentDashboard({user}: Props) { user={user} items={[ { - icon: , - value: Object.keys(groupBySession(stats)).length, + icon: , + value: countFullExams(stats), label: "Exams", tooltip: "Number of all conducted completed exams", }, { - icon: , - value: stats.length, - label: "Exercises", - tooltip: "Number of all conducted exercises including Level Test", + icon: , + value: countExamModules(stats), + label: "Modules", + tooltip: "Number of all exam modules performed including Level Test", }, { icon: , diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index fdb36314..e19324c9 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -14,7 +14,7 @@ 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, sortByModule} from "@/utils/moduleUtils"; +import {countExamModules, countFullExams, MODULE_ARRAY, sortByModule} from "@/utils/moduleUtils"; import {Chart} from "react-chartjs-2"; import useUsers from "@/hooks/useUsers"; import Select from "react-select"; @@ -39,7 +39,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { redirect: { destination: "/login", permanent: false, - } + }, }; } @@ -48,7 +48,7 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => { redirect: { destination: "/", permanent: false, - } + }, }; } @@ -164,21 +164,21 @@ export default function Stats() { items={[ { icon: , - value: Object.keys(groupBySession(userStats)).length, + value: countFullExams(userStats), label: "Exams", - tooltip: 'Number of all conducted completed exams', + tooltip: "Number of all conducted completed exams", }, { icon: , - value: userStats.length, - label: "Exercises", - tooltip: 'Number of all conducted exercises including Level Test', + value: countExamModules(userStats), + label: "Modules", + tooltip: "Number of all exam modules performed including Level Test", }, { icon: , value: `${userStats.length > 0 ? averageScore(userStats) : 0}%`, label: "Average Score", - tooltip: 'Average success rate for questions responded', + tooltip: "Average success rate for questions responded", }, ]} /> diff --git a/src/utils/moduleUtils.ts b/src/utils/moduleUtils.ts index 8abafbd8..9fd8ccf8 100644 --- a/src/utils/moduleUtils.ts +++ b/src/utils/moduleUtils.ts @@ -1,5 +1,8 @@ import {Module} from "@/interfaces"; import {Exercise} from "@/interfaces/exam"; +import {Stat} from "@/interfaces/user"; +import {uniq} from "lodash"; +import {groupBySession} from "./stats"; export const MODULE_ARRAY: Module[] = ["reading", "listening", "writing", "speaking", "level"]; @@ -29,3 +32,30 @@ export const countExercises = (exercises: Exercise[]) => { return lengthMap.reduce((accumulator, current) => accumulator + current, 0); }; + +export const countFullExams = (stats: Stat[]) => { + const sessionExams = groupBySession(stats); + return Object.keys(sessionExams).filter((x) => { + const sessionStats = sessionExams[x as keyof typeof sessionExams]; + const sessionModules = uniq(sessionStats.map((x) => x.module)); + + return ( + sessionModules.includes("reading") && + sessionModules.includes("listening") && + sessionModules.includes("writing") && + sessionModules.includes("speaking") + ); + }).length; +}; + +export const countExamModules = (stats: Stat[]) => { + const sessionExams = groupBySession(stats); + const modulesPerSession = Object.keys(sessionExams).map((x) => { + const sessionStats = sessionExams[x as keyof typeof sessionExams]; + const sessionModules = uniq(sessionStats.map((x) => x.module)); + + return sessionModules.length; + }); + + return modulesPerSession.reduce((acc, curr) => curr + acc, 0); +}; From 62ecc4e395527dfd3b2af90cc43ad75cac18c50e Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 2 Apr 2024 10:32:59 +0100 Subject: [PATCH 2/3] Added the ability to edit the options of a Level Exam --- src/pages/(generation)/LevelGeneration.tsx | 95 +++++++++++++++++----- 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/src/pages/(generation)/LevelGeneration.tsx b/src/pages/(generation)/LevelGeneration.tsx index 26cdf67b..9a92ec12 100644 --- a/src/pages/(generation)/LevelGeneration.tsx +++ b/src/pages/(generation)/LevelGeneration.tsx @@ -1,5 +1,5 @@ import Select from "@/components/Low/Select"; -import {Difficulty, LevelExam, MultipleChoiceExercise} from "@/interfaces/exam"; +import {Difficulty, LevelExam, MultipleChoiceExercise, MultipleChoiceQuestion} from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import {getExamById} from "@/utils/exams"; import {playSound} from "@/utils/sound"; @@ -9,12 +9,69 @@ import clsx from "clsx"; import {capitalize, sample} from "lodash"; import {useRouter} from "next/router"; import {useState} from "react"; -import {BsArrowRepeat} from "react-icons/bs"; +import {BsArrowRepeat, BsCheck, BsPencilSquare, BsX} from "react-icons/bs"; import {toast} from "react-toastify"; import {v4} from "uuid"; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; +const QuestionDisplay = ({question, onUpdate}: {question: MultipleChoiceQuestion; onUpdate: (question: MultipleChoiceQuestion) => void}) => { + const [isEditing, setIsEditing] = useState(false); + const [options, setOptions] = useState(question.options); + + return ( +
+ + {question.id}. {question.prompt}{" "} + +
+ {question.options.map((option, index) => ( + + + ({option.id}) + {" "} + {isEditing ? ( + setOptions((prev) => prev.map((x, idx) => (idx === index ? {...x, text: e.target.value} : x)))} + /> + ) : ( + {option.text} + )} + + ))} +
+
+ {!isEditing && ( + + )} + {isEditing && ( + <> + + + + )} +
+
+ ); +}; + const TaskTab = ({exam, difficulty, setExam}: {exam?: LevelExam; difficulty: Difficulty; setExam: (exam: LevelExam) => void}) => { const [isLoading, setIsLoading] = useState(false); @@ -37,6 +94,20 @@ const TaskTab = ({exam, difficulty, setExam}: {exam?: LevelExam; difficulty: Dif .finally(() => setIsLoading(false)); }; + const onUpdate = (question: MultipleChoiceQuestion) => { + if (!exam) return; + + const updatedExam = { + ...exam, + exercises: exam.exercises.map((x) => ({ + ...x, + questions: (x as MultipleChoiceExercise).questions.map((q) => (q.id === question.id ? question : q)), + })), + }; + console.log(updatedExam); + setExam(updatedExam as any); + }; + return (
@@ -80,25 +151,7 @@ const TaskTab = ({exam, difficulty, setExam}: {exam?: LevelExam; difficulty: Dif
{exercise.questions.map((question) => ( -
- - {question.id}. {question.prompt} - -
- {question.options.map((option) => ( - - - ({option.id}) - {" "} - {option.text} - - ))} -
-
+ ))}
From f374d91ef827dd108bc8322546d0a6733ff699e6 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 2 Apr 2024 10:53:34 +0100 Subject: [PATCH 3/3] Solved an issue where the company name of country managers wasn't able to be updated --- src/components/UserCard.tsx | 2 +- src/pages/(admin)/Lists/UserList.tsx | 44 ++++++++++++---------------- src/pages/api/users/update.ts | 2 +- src/resources/user.ts | 8 +++-- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 1580272d..93e9bac3 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -114,7 +114,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, agentInformation: type === "agent" ? { - name: companyName, + companyName, commercialRegistration, } : undefined, diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index 89b9656a..15256380 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -16,18 +16,14 @@ import {countries, TCountries} from "countries-list"; import countryCodes from "country-codes-list"; import Modal from "@/components/Modal"; import UserCard from "@/components/UserCard"; -import {USER_TYPE_LABELS} from "@/resources/user"; +import {isAgentUser, USER_TYPE_LABELS} from "@/resources/user"; import useFilterStore from "@/stores/listFilterStore"; import {useRouter} from "next/router"; -import {isCorporateUser} from '@/resources/user'; -import { useListSearch } from "@/hooks/useListSearch"; +import {isCorporateUser} from "@/resources/user"; +import {useListSearch} from "@/hooks/useListSearch"; const columnHelper = createColumnHelper(); -const searchFields = [ - ['name'], - ['email'], - ['corporateInformation', 'companyInformation', 'name'], -]; +const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; export default function UserList({user, filters = []}: {user: User; filters?: ((user: User) => boolean)[]}) { const [showDemographicInformation, setShowDemographicInformation] = useState(false); const [sorter, setSorter] = useState(); @@ -331,14 +327,14 @@ export default function UserList({user, filters = []}: {user: User; filters?: (( ) as any, cell: (info) => USER_TYPE_LABELS[info.getValue()], }), - columnHelper.accessor('corporateInformation.companyInformation.name', { + columnHelper.accessor("corporateInformation.companyInformation.name", { header: ( ) as any, - cell: (info) => getCorporateName(info.row.original), + cell: (info) => getCompanyName(info.row.original), }), columnHelper.accessor("subscriptionExpirationDate", { header: ( @@ -393,13 +389,16 @@ export default function UserList({user, filters = []}: {user: User; filters?: (( return undefined; }; - const getCorporateName = (user: User) => { - if(isCorporateUser(user)) { - return user.corporateInformation?.companyInformation?.name + const getCompanyName = (user: User) => { + if (isCorporateUser(user)) { + return user.corporateInformation?.companyInformation?.name; + } + if (isAgentUser(user)) { + return user.agentInformation.companyName; } - return ''; - } + return ""; + }; const sortFunction = (a: User, b: User) => { if (sorter === "name" || sorter === reverseString("name")) @@ -468,25 +467,20 @@ export default function UserList({user, filters = []}: {user: User; filters?: (( : b.demographicInformation!.gender.localeCompare(a.demographicInformation!.gender); } - if(sorter === 'companyName' || sorter === reverseString('companyName')) { - const aCorporateName = getCorporateName(a); - const bCorporateName = getCorporateName(b); + if (sorter === "companyName" || sorter === reverseString("companyName")) { + const aCorporateName = getCompanyName(a); + const bCorporateName = getCompanyName(b); if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1; if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1; if (!aCorporateName && !bCorporateName) return 0; - return sorter === "companyName" - ? aCorporateName.localeCompare(bCorporateName) - : bCorporateName.localeCompare(aCorporateName); + return sorter === "companyName" ? aCorporateName.localeCompare(bCorporateName) : bCorporateName.localeCompare(aCorporateName); } return a.id.localeCompare(b.id); }; - const { rows: filteredRows, renderSearch } = useListSearch( - searchFields, - displayUsers, - ) + const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); const table = useReactTable({ data: filteredRows, diff --git a/src/pages/api/users/update.ts b/src/pages/api/users/update.ts index b8091f50..3a79045f 100644 --- a/src/pages/api/users/update.ts +++ b/src/pages/api/users/update.ts @@ -85,7 +85,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const user = await setDoc(userRef, updatedUser, {merge: true}); await managePaymentRecords(updatedUser, updatedUser.id); - if (updatedUser.status) { + if (updatedUser.status || updatedUser.type === "corporate") { // there's no await as this does not affect the user propagateStatusChange(queryId, updatedUser.status); } diff --git a/src/resources/user.ts b/src/resources/user.ts index ffc2c8b5..c6a516c1 100644 --- a/src/resources/user.ts +++ b/src/resources/user.ts @@ -1,4 +1,4 @@ -import {Type, User, CorporateUser} from "@/interfaces/user"; +import {Type, User, CorporateUser, AgentUser} from "@/interfaces/user"; export const USER_TYPE_LABELS: {[key in Type]: string} = { student: "Student", @@ -11,4 +11,8 @@ export const USER_TYPE_LABELS: {[key in Type]: string} = { export function isCorporateUser(user: User): user is CorporateUser { return (user as CorporateUser).corporateInformation !== undefined; - } \ No newline at end of file +} + +export function isAgentUser(user: User): user is AgentUser { + return (user as AgentUser).agentInformation !== undefined; +}