From 10a480aa81b762cf196d76b1f9e36da198ca6e02 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 14 Jan 2024 22:08:17 +0000 Subject: [PATCH 01/37] Updated the Code generators select to depend on the type of user --- src/pages/(admin)/BatchCodeGenerator.tsx | 21 ++++++++++++++++----- src/pages/(admin)/CodeGenerator.tsx | 21 ++++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index e4988a15..c4784c41 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -17,6 +17,15 @@ import readXlsxFile from "read-excel-file"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); +const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = { + student: [], + teacher: [], + agent: [], + corporate: ["student", "teacher"], + admin: ["student", "teacher", "agent", "corporate", "admin"], + developer: ["student", "teacher", "agent", "corporate", "admin", "developer"], +}; + export default function BatchCodeGenerator({user}: {user: User}) { const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]); const [isLoading, setIsLoading] = useState(false); @@ -136,11 +145,13 @@ export default function BatchCodeGenerator({user}: {user: User}) { defaultValue="student" onChange={(e) => setType(e.target.value as typeof user.type)} className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white"> - {Object.keys(USER_TYPE_LABELS).map((type) => ( - - ))} + {Object.keys(USER_TYPE_LABELS) + .filter((x) => USER_TYPE_PERMISSIONS[user.type].includes(x as Type)) + .map((type) => ( + + ))} )} - {user && (user.type === "developer" || user.type === "admin") && ( - <> -
- - - Enabled - + <> + setShowHelp(false)} title="Excel File Format"> +
+ Please upload an Excel file with the following format: + + + + + + + + + + + +
First NameLast NameCountryPassport/National IDE-mailPhone Number
+ + Notes: +
    +
  • - All incorrect e-mails will be ignored;
  • +
  • - All already registered e-mails will be ignored;
  • +
  • - You may have a header row with the format above, however, it is not necessary;
  • +
  • - All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.
  • +
+
+
+
+
+
+ +
setShowHelp(true)}> +
- {isExpiryDateEnabled && ( - moment(date).isAfter(new Date())} - dateFormat="dd/MM/yyyy" - selected={expiryDate} - onChange={(date) => setExpiryDate(date)} - /> - )} - - )} - - {user && ( - - )} - -
+
+ + {user && (user.type === "developer" || user.type === "admin") && ( + <> +
+ + + Enabled + +
+ {isExpiryDateEnabled && ( + moment(date).isAfter(new Date())} + dateFormat="dd/MM/yyyy" + selected={expiryDate} + onChange={(date) => setExpiryDate(date)} + /> + )} + + )} + + {user && ( + + )} + +
+ ); } From 3f0821eb33db1169d8220b8942f35cd9a5500789 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 14 Jan 2024 23:31:50 +0000 Subject: [PATCH 09/37] Added the corporate name to the user's top-right profile link --- src/components/Navbar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 6f6f5edf..e6ac2460 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -72,7 +72,8 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal {user.name} - {user.name} | {USER_TYPE_LABELS[user.type]} + {user.type === "corporate" ? `${user.corporateInformation?.companyInformation.name} |` : ""} {user.name} |{" "} + {USER_TYPE_LABELS[user.type]}
setIsMenuOpen(true)}> From f5bdedee2f93449529047bfd31881a9515f5761c Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 14 Jan 2024 23:35:48 +0000 Subject: [PATCH 10/37] Updated the message of the failed delete payment --- src/pages/payment-record.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 4090787a..8ccc28c3 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -371,7 +371,7 @@ export default function PaymentRecord() { } if (reason.response.status === 403) { - toast.error("You do not have permission to delete this exam!"); + toast.error("You do not have permission to delete an approved payment record!"); return; } @@ -785,7 +785,7 @@ export default function PaymentRecord() {

Payment Record

- {(user.type === "developer" || user.type === "admin" || user.type === 'agent' || user.type === 'corporate') && ( + {(user.type === "developer" || user.type === "admin" || user.type === "agent" || user.type === "corporate") && (
- {referralAgent !== "" ? ( + {referralAgent !== "" && loggedInUser.type !== "corporate" ? ( <> Date: Mon, 15 Jan 2024 10:20:23 +0000 Subject: [PATCH 12/37] Updated the label of the cancel button on FillBlanks --- src/components/Exercises/FillBlanks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx index 0d7798d3..b0433ac6 100644 --- a/src/components/Exercises/FillBlanks.tsx +++ b/src/components/Exercises/FillBlanks.tsx @@ -53,7 +53,7 @@ function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel,
+ {user.type !== "teacher" && ( + + )}
From 46b9fe50ef44596e5322fbf5ed15a279fea94d6e Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Mon, 15 Jan 2024 19:32:11 +0000 Subject: [PATCH 16/37] Added the missing radial progress --- src/exams/pdf/test.report.tsx | 16 ++++++++++++++-- src/pages/api/stats/[id]/export.tsx | 7 ++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/exams/pdf/test.report.tsx b/src/exams/pdf/test.report.tsx index acd14481..d6cbaade 100644 --- a/src/exams/pdf/test.report.tsx +++ b/src/exams/pdf/test.report.tsx @@ -25,6 +25,8 @@ interface Props { qrcode: string; renderDetails: React.ReactNode; title: string; + summaryPNG: string; + summaryScore: string; } const TestReport = ({ @@ -39,6 +41,8 @@ const TestReport = ({ logo, qrcode, renderDetails, + summaryPNG, + summaryScore, }: Props) => { const defaultTextStyle = [styles.textFont, { fontSize: 8 }]; const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }]; @@ -104,8 +108,16 @@ const TestReport = ({ > Performance Summary - - {summary} + + + {summary} + + + + + {summaryScore} + + diff --git a/src/pages/api/stats/[id]/export.tsx b/src/pages/api/stats/[id]/export.tsx index a1c3bab5..b7a103db 100644 --- a/src/pages/api/stats/[id]/export.tsx +++ b/src/pages/api/stats/[id]/export.tsx @@ -263,12 +263,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) { ); const overallResult = overallScore / overallTotal; + const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal); + // generate the overall detail report const overallDetail = { module: "Overall", score: overallScore, total: overallTotal, - png: getRadialProgressPNG("laranja", overallScore, overallTotal), + png: overallPNG, } as ModuleScore; const testDetails = [overallDetail, ...finalResults]; @@ -301,6 +303,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { }; const { title, details } = getCustomData(); + const pdfStream = await ReactPDF.renderToStream( ); From 7572909b13821de30ac9fd3cf6692f8f71f3b086 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Mon, 15 Jan 2024 19:33:31 +0000 Subject: [PATCH 17/37] Removed unnecessary margin ruining percentage centered --- src/exams/pdf/styles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/exams/pdf/styles.ts b/src/exams/pdf/styles.ts index a8162499..0440c37f 100644 --- a/src/exams/pdf/styles.ts +++ b/src/exams/pdf/styles.ts @@ -55,7 +55,6 @@ export const styles = StyleSheet.create({ display: "flex", flexDirection: "column", alignItems: "center", - gap: 4, position: "relative", }, radialResultContainer: { From 294d319ab38c5cda73730ff21d043fa3f7c79baa Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Mon, 15 Jan 2024 19:47:53 +0000 Subject: [PATCH 18/37] Removed debuggers --- src/exams/pdf/test.report.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/exams/pdf/test.report.tsx b/src/exams/pdf/test.report.tsx index d6cbaade..5b72feaf 100644 --- a/src/exams/pdf/test.report.tsx +++ b/src/exams/pdf/test.report.tsx @@ -112,9 +112,9 @@ const TestReport = ({ {summary} - + - + {summaryScore} From 367553eb44a52855ed9cd760d766d241f4451799 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 15 Jan 2024 20:27:20 +0000 Subject: [PATCH 19/37] =?UTF-8?q?Added=20associated=20corporate=E2=80=99s?= =?UTF-8?q?=20name=20to=20Students=20and=20Teachers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dashboards/Student.tsx | 15 ++++++++++++++- src/dashboards/Teacher.tsx | 21 ++++++++++++++++++--- src/utils/groups.ts | 10 +++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index 369249f7..81223002 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -5,9 +5,10 @@ import ProfileSummary from "@/components/ProfileSummary"; import useAssignments from "@/hooks/useAssignments"; import useStats from "@/hooks/useStats"; import {Assignment} from "@/interfaces/results"; -import {User} from "@/interfaces/user"; +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 {averageScore, groupBySession} from "@/utils/stats"; import {CreateOrderActions, CreateOrderData, OnApproveActions, OnApproveData, OrderResponseBody} from "@paypal/paypal-js"; @@ -18,6 +19,7 @@ import {capitalize} from "lodash"; import moment from "moment"; import Link from "next/link"; import {useRouter} from "next/router"; +import {useEffect, useState} from "react"; import {BsArrowRepeat, BsBook, BsClipboard, BsFileEarmarkText, BsHeadphones, BsMegaphone, BsPen, BsPencil, BsStar} from "react-icons/bs"; import {toast} from "react-toastify"; @@ -26,6 +28,8 @@ interface Props { } export default function StudentDashboard({user}: Props) { + const [corporateUserToShow, setCorporateUserToShow] = useState(); + const {stats} = useStats(user.id); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id}); @@ -37,6 +41,10 @@ export default function StudentDashboard({user}: Props) { const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setAssignment = useExamStore((state) => state.setAssignment); + useEffect(() => { + getUserCorporate(user.id).then(setCorporateUserToShow); + }, [user]); + const startAssignment = (assignment: Assignment) => { const examPromises = assignment.exams.filter((e) => e.assignee === user.id).map((e) => getExamById(e.module, e.id)); @@ -60,6 +68,11 @@ export default function StudentDashboard({user}: Props) { return ( <> + {corporateUserToShow && ( +
+ Corporate: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name} +
+ )} (); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); + const [corporateUserToShow, setCorporateUserToShow] = useState(); const {stats} = useStats(); const {users, reload} = useUsers(); @@ -65,6 +67,10 @@ export default function TeacherDashboard({user}: Props) { setShowModal(!!selectedUser && page === ""); }, [selectedUser, page]); + useEffect(() => { + getUserCorporate(user.id).then(setCorporateUserToShow); + }, [user]); + const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id); const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); @@ -226,7 +232,7 @@ export default function TeacherDashboard({user}: Props) {

Past Assignments ({assignments.filter(pastFilter).length})

{assignments.filter(pastFilter).map((a) => ( - setSelectedAssignment(a)} key={a.id} allowDownload/> + setSelectedAssignment(a)} key={a.id} allowDownload /> ))}
@@ -236,7 +242,16 @@ export default function TeacherDashboard({user}: Props) { const DefaultDashboard = () => ( <> -
+ {corporateUserToShow && ( +
+ Corporate: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name} +
+ )} +
setPage("students")} Icon={BsPersonFill} diff --git a/src/utils/groups.ts b/src/utils/groups.ts index 1bb59388..0d0dec55 100644 --- a/src/utils/groups.ts +++ b/src/utils/groups.ts @@ -1,4 +1,4 @@ -import {Group, User} from "@/interfaces/user"; +import {CorporateUser, Group, User} from "@/interfaces/user"; import axios from "axios"; export const isUserFromCorporate = async (userID: string) => { @@ -8,3 +8,11 @@ export const isUserFromCorporate = async (userID: string) => { const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type); return adminTypes.includes("corporate"); }; + +export const getUserCorporate = async (userID: string) => { + const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; + const users = (await axios.get("/api/users/list")).data; + + const admins = groups.map((g) => users.find((u) => u.id === g.admin)); + return admins.map((x) => x?.type).includes("corporate") ? (admins[0] as CorporateUser) : undefined; +}; From ccde1c84b706c7bd4b7a9c7be9c7f79e0a6290c0 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 15 Jan 2024 20:35:11 +0000 Subject: [PATCH 20/37] Added a log for the exam for developers --- src/pages/(exam)/ExamPage.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 653869d2..23b7c65e 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -49,6 +49,9 @@ export default function ExamPage({page}: Props) { const router = useRouter(); useEffect(() => setSessionId(uuidv4()), []); + useEffect(() => { + if (user?.type === "developer") console.log(exam); + }, [exam, user]); useEffect(() => { selectedModules.length > 0 && timeSpent === 0 && !showSolutions; From f9e037bd7b424cc4b617daae80c6205516dd1bbf Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 15 Jan 2024 21:01:38 +0000 Subject: [PATCH 21/37] Updated to "Linked to:" --- src/dashboards/Student.tsx | 2 +- src/dashboards/Teacher.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index 81223002..97acd6d1 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -70,7 +70,7 @@ export default function StudentDashboard({user}: Props) { <> {corporateUserToShow && (
- Corporate: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name} + Linked to: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name}
)} {corporateUserToShow && (
- Corporate: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name} + Linked to: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name}
)}
Date: Mon, 15 Jan 2024 21:21:08 +0000 Subject: [PATCH 22/37] =?UTF-8?q?Solved:=20A=20second=20=E2=80=9CNext?= =?UTF-8?q?=E2=80=9D=20button=20appears=20on=20Listening=20part=20transiti?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/exams/Listening.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 5534d09a..56ee61d1 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -175,7 +175,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props )} - {exerciseIndex === -1 && ( + {exerciseIndex === -1 && partIndex === 0 && ( From c68e206aaedbd824bc3679808061cd3bc2b20ce2 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 15 Jan 2024 21:32:54 +0000 Subject: [PATCH 23/37] Updated the Group creation modal to use Excel --- src/pages/(admin)/BatchCodeGenerator.tsx | 8 ++- src/pages/(admin)/Lists/GroupList.tsx | 72 +++++++++++++++--------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index d5aaeca8..e03cad7a 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -38,7 +38,7 @@ export default function BatchCodeGenerator({user}: {user: User}) { const {users} = useUsers(); - const {openFilePicker, filesContent} = useFilePicker({ + const {openFilePicker, filesContent, clear} = useFilePicker({ accept: ".xlsx", multiple: false, readAs: "ArrayBuffer", @@ -74,10 +74,12 @@ export default function BatchCodeGenerator({user}: {user: User}) { (x) => x.email, ); - if (information.length === 0) - return toast.error( + if (information.length === 0) { + toast.error( "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!", ); + return clear(); + } setInfos(information); }); diff --git a/src/pages/(admin)/Lists/GroupList.tsx b/src/pages/(admin)/Lists/GroupList.tsx index 4f6efb89..1bde57c8 100644 --- a/src/pages/(admin)/Lists/GroupList.tsx +++ b/src/pages/(admin)/Lists/GroupList.tsx @@ -9,16 +9,18 @@ import {Disclosure, Transition} from "@headlessui/react"; import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; +import {capitalize, uniq, uniqBy} from "lodash"; import {useEffect, useRef, useState} from "react"; -import {BsCheck, BsDash, BsPencil, BsPlus, BsTrash} from "react-icons/bs"; +import {BsCheck, BsDash, BsPencil, BsPlus, BsQuestionCircleFill, BsTrash} from "react-icons/bs"; import {toast} from "react-toastify"; import Select from "react-select"; import {uuidv4} from "@firebase/util"; import {useFilePicker} from "use-file-picker"; import Modal from "@/components/Modal"; +import readXlsxFile from "read-excel-file"; const columnHelper = createColumnHelper(); +const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); interface CreateDialogProps { user: User; @@ -31,40 +33,49 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => { const [name, setName] = useState(group?.name || undefined); const [admin, setAdmin] = useState(group?.admin || user.id); const [participants, setParticipants] = useState(group?.participants || []); - const {openFilePicker, filesContent} = useFilePicker({ - accept: ".txt", + const {openFilePicker, filesContent, clear} = useFilePicker({ + accept: ".xlsx", multiple: false, + readAs: "ArrayBuffer", }); useEffect(() => { if (filesContent.length > 0) { const file = filesContent[0]; - const emails = file.content - .toLowerCase() - .split("\n") - .filter((x) => new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/).test(x)); + readXlsxFile(file.content).then((rows) => { + const emails = uniq( + rows + .map((row) => { + const [email] = row as string[]; + return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined; + }) + .filter((x) => !!x), + ); - if (emails.length === 0) { - toast.error("Please upload a .txt file containing e-mails, one per line!"); - return; - } + if (emails.length === 0) { + toast.error("Please upload an Excel file containing e-mails!"); + clear(); + return; + } - const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); - const filteredUsers = emailUsers.filter( - (x) => - ((user.type === "developer" || user.type === "admin" || user.type === "corporate") && - (x?.type === "student" || x?.type === "teacher")) || - (user.type === "teacher" && x?.type === "student"), - ); + const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); + const filteredUsers = emailUsers.filter( + (x) => + ((user.type === "developer" || user.type === "admin" || user.type === "corporate") && + (x?.type === "student" || x?.type === "teacher")) || + (user.type === "teacher" && x?.type === "student"), + ); - setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); - toast.success( - user.type !== "teacher" - ? "Added all teachers and students found in the file you've provided!" - : "Added all students found in the file you've provided!", - {toastId: "upload-success"}, - ); + setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); + toast.success( + user.type !== "teacher" + ? "Added all teachers and students found in the file you've provided!" + : "Added all students found in the file you've provided!", + {toastId: "upload-success"}, + ); + }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [filesContent, user.type, users]); const submit = () => { @@ -90,7 +101,12 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
- +
+ +
+ +
+
)} - {user.type !== "corporate" && ( -
- - - {EMPLOYMENT_STATUS.map(({status, label}) => ( - - {({checked}) => ( - - {label} - - )} - - ))} - -
- )} + {user.type !== "corporate" && }
diff --git a/src/components/High/EmploymentStatusInput.tsx b/src/components/High/EmploymentStatusInput.tsx new file mode 100644 index 00000000..85fb0229 --- /dev/null +++ b/src/components/High/EmploymentStatusInput.tsx @@ -0,0 +1,32 @@ +import {EmploymentStatus, EMPLOYMENT_STATUS} from "@/interfaces/user"; +import {RadioGroup} from "@headlessui/react"; +import clsx from "clsx"; + +interface Props { + value?: EmploymentStatus; + onChange: (value?: EmploymentStatus) => void; +} + +export default function EmploymentStatusInput({value, onChange}: Props) { + return ( +
+ + + {EMPLOYMENT_STATUS.map(({status, label}) => ( + + {({checked}) => ( + + {label} + + )} + + ))} + +
+ ); +} diff --git a/src/components/High/GenderInput.tsx b/src/components/High/GenderInput.tsx new file mode 100644 index 00000000..0848d1bb --- /dev/null +++ b/src/components/High/GenderInput.tsx @@ -0,0 +1,54 @@ +import {Gender} from "@/interfaces/user"; +import {RadioGroup} from "@headlessui/react"; +import clsx from "clsx"; + +interface Props { + value?: Gender; + onChange: (value?: Gender) => void; +} + +export default function GenderInput({value, onChange}: Props) { + return ( +
+ + + + {({checked}) => ( + + Male + + )} + + + {({checked}) => ( + + Female + + )} + + + {({checked}) => ( + + Other + + )} + + +
+ ); +} diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index d71c355b..a71af678 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import {withIronSessionSsr} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; -import {ChangeEvent, useEffect, useRef, useState} from "react"; +import {ChangeEvent, ReactNode, useEffect, useRef, useState} from "react"; import useUser from "@/hooks/useUser"; import {toast, ToastContainer} from "react-toastify"; import Layout from "@/components/High/Layout"; @@ -13,7 +13,7 @@ import axios from "axios"; import {ErrorMessage} from "@/constants/errors"; import {RadioGroup} from "@headlessui/react"; import clsx from "clsx"; -import {EmploymentStatus, EMPLOYMENT_STATUS, Gender, User} from "@/interfaces/user"; +import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User} from "@/interfaces/user"; import CountrySelect from "@/components/Low/CountrySelect"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import moment from "moment"; @@ -21,6 +21,10 @@ import {BsCamera, BsCameraFill} from "react-icons/bs"; import {USER_TYPE_LABELS} from "@/resources/user"; import useGroups from "@/hooks/useGroups"; import useUsers from "@/hooks/useUsers"; +import {convertBase64} from "@/utils"; +import {Divider} from "primereact/divider"; +import GenderInput from "@/components/High/GenderInput"; +import EmploymentStatusInput from "@/components/High/EmploymentStatusInput"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -74,7 +78,14 @@ function UserProfile({user, mutateUser}: Props) { ); const [passport_id, setPassportID] = useState(user.type === "student" ? user.demographicInformation?.passport_id : undefined); const [position, setPosition] = useState(user.type === "corporate" ? user.demographicInformation?.position : undefined); - const [companyName, setCompanyName] = useState(user.type === "agent" ? user.agentInformation?.companyName : undefined); + const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined); + const [companyName, setCompanyName] = useState( + user.type === "agent" + ? user.agentInformation?.companyName + : user.type === "corporate" + ? user.corporateInformation?.companyInformation.name + : undefined, + ); const [commercialRegistration, setCommercialRegistration] = useState( user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); @@ -92,19 +103,6 @@ function UserProfile({user, mutateUser}: Props) { if (today.add(7, "days").isAfter(momentDate)) return "!bg-mti-orange-ultralight border-mti-orange-light"; }; - const convertBase64 = (file: File) => { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.readAsDataURL(file); - fileReader.onload = () => { - resolve(fileReader.result); - }; - fileReader.onerror = (error) => { - reject(error); - }; - }); - }; - const uploadProfilePicture = async (event: ChangeEvent) => { if (event.target.files && event.target.files[0]) { const picture = event.target.files[0]; @@ -155,6 +153,7 @@ function UserProfile({user, mutateUser}: Props) { gender, passport_id, }, + ...(user.type === "corporate" ? {corporateInformation} : {}), }); if (request.status === 200) { toast.success("Your profile has been updated!"); @@ -167,6 +166,93 @@ function UserProfile({user, mutateUser}: Props) { setIsLoading(false); }; + const DoubleColumnRow = ({children}: {children: ReactNode}) =>
{children}
; + + const PasswordInput = () => ( + + setPassword(e)} + placeholder="Enter your password" + required + /> + setNewPassword(e)} + placeholder="Enter your new password (optional)" + /> + + ); + + const NameInput = () => ( + setName(e)} placeholder="Enter your name" defaultValue={name} required /> + ); + + const AgentInformationInput = () => ( +
+ null} + placeholder="Enter corporate name" + defaultValue={companyName} + disabled + /> + null} + placeholder="Enter commercial registration" + defaultValue={commercialRegistration} + disabled + /> +
+ ); + + const CountryInput = () => ( +
+ + +
+ ); + + const PhoneInput = () => ( + setPhone(e)} + placeholder="Enter phone number" + defaultValue={phone} + required + /> + ); + + const ExpirationDate = () => ( +
+ + + {!user.subscriptionExpirationDate && "Unlimited"} + {user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} + +
+ ); + return (
@@ -175,16 +261,26 @@ function UserProfile({user, mutateUser}: Props) {

Edit Profile

-
- setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> + + {user.type !== "corporate" ? ( + + ) : ( + + setCorporateInformation((prev) => ({ + ...prev!, + companyInformation: {...prev!.companyInformation, name: e}, + })) + } + placeholder="Enter your company's name" + defaultValue={corporateInformation?.companyInformation.name} + required + /> + )} + -
+ + + {user.type === "student" && ( )} -
- setPassword(e)} - placeholder="Enter your password" - required - /> - setNewPassword(e)} - placeholder="Enter your new password (optional)" - /> -
+ {user.type === "agent" && } - {user.type === "agent" && ( -
- null} - placeholder="Enter corporate name" - defaultValue={companyName} - disabled - /> - null} - placeholder="Enter commercial registration" - defaultValue={commercialRegistration} - disabled - /> -
+ + + + + + + + {user.type === "corporate" && ( + <> + + null} + label="Number of users" + defaultValue={user.corporateInformation.companyInformation.userAmount} + disabled + required + /> + null} + label="Pricing" + defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} + disabled + required + /> + + + )} -
-
- - -
- setPhone(e)} - placeholder="Enter phone number" - defaultValue={phone} - required - /> -
-
- {user.type === "corporate" && ( - - )} - {user.type !== "corporate" && ( -
- - - {EMPLOYMENT_STATUS.map(({status, label}) => ( - - {({checked}) => ( - - {label} - - )} - - ))} - + {user.type === "corporate" && ( + <> + + + + + + + )} + + {user.type === "corporate" && user.corporateInformation.referralAgent && ( + <> + + + null} + defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name} + type="text" + label="Country Manager's Name" + placeholder="Not available" + required + disabled + /> + null} + defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email} + type="text" + label="Country Manager's E-mail" + placeholder="Not available" + required + disabled + /> + + +
+ + x.id === user.corporateInformation.referralAgent)?.demographicInformation + ?.country + } + onChange={() => null} + disabled + /> +
+ + null} + placeholder="Not available" + defaultValue={ + users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone + } + disabled + required + /> +
+ + )} + + {user.type !== "corporate" && ( + + + +
+ +
- )} -
-
- - - - {({checked}) => ( - - Male - - )} - - - {({checked}) => ( - - Female - - )} - - - {({checked}) => ( - - Other - - )} - - -
-
- - - {!user.subscriptionExpirationDate && "Unlimited"} - {user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} - -
-
-
+ + )}
diff --git a/src/utils/index.ts b/src/utils/index.ts index c63966f9..cb52f2a1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,3 +12,16 @@ export function dateSorter(a: any, b: any, direction: "asc" | "desc", key: strin export function env(key: string) { return (window as any).__ENV[key]; } + +export const convertBase64 = (file: File) => { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.readAsDataURL(file); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.onerror = (error) => { + reject(error); + }; + }); +}; From d0b0dfb16f77c462b219aa066113809b77bf99bc Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 16 Jan 2024 16:30:38 +0000 Subject: [PATCH 25/37] Solved a bug with the UserCard --- src/components/UserCard.tsx | 4 ++-- src/pages/profile.tsx | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 9aa675ae..4c0c48d3 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -74,11 +74,11 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, user.type === "corporate" ? user.corporateInformation?.companyInformation.name : user.type === "agent" - ? user.agentInformation.companyName + ? user.agentInformation?.companyName : undefined, ); const [commercialRegistration, setCommercialRegistration] = useState( - user.type === "agent" ? user.agentInformation.commercialRegistration : undefined, + user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined); const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined); diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index a71af678..cad6dc35 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -79,13 +79,7 @@ function UserProfile({user, mutateUser}: Props) { const [passport_id, setPassportID] = useState(user.type === "student" ? user.demographicInformation?.passport_id : undefined); const [position, setPosition] = useState(user.type === "corporate" ? user.demographicInformation?.position : undefined); const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined); - const [companyName, setCompanyName] = useState( - user.type === "agent" - ? user.agentInformation?.companyName - : user.type === "corporate" - ? user.corporateInformation?.companyInformation.name - : undefined, - ); + const [companyName, setCompanyName] = useState(user.type === "agent" ? user.agentInformation?.companyName : undefined); const [commercialRegistration, setCommercialRegistration] = useState( user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); From 01a9da3a5be3cc5fde31b38354d43f12be93a3b2 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 16 Jan 2024 18:42:12 +0000 Subject: [PATCH 26/37] Added Date export based on user timezone --- package.json | 1 + src/components/Low/TImezoneSelect.tsx | 64 +++++++++++++++++++++++ src/interfaces/user.ts | 2 + src/pages/api/assignments/[id]/export.tsx | 3 +- src/pages/api/stats/[id]/export.tsx | 3 +- src/pages/profile.tsx | 18 +++++-- yarn.lock | 7 +++ 7 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/components/Low/TImezoneSelect.tsx diff --git a/package.json b/package.json index 28d3f1f6..c7cb23e0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "iron-session": "^6.3.1", "lodash": "^4.17.21", "moment": "^2.29.4", + "moment-timezone": "^0.5.44", "next": "13.1.6", "nodemailer": "^6.9.5", "nodemailer-express-handlebars": "^6.1.0", diff --git a/src/components/Low/TImezoneSelect.tsx b/src/components/Low/TImezoneSelect.tsx new file mode 100644 index 00000000..237cf781 --- /dev/null +++ b/src/components/Low/TImezoneSelect.tsx @@ -0,0 +1,64 @@ +import { Fragment, useState } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { BsChevronExpand } from "react-icons/bs"; +import moment from "moment-timezone"; + +interface Props { + value?: string; + onChange?: (value: string) => void; + disabled?: boolean; +} + +export default function TimezoneSelect({ + value, + disabled = false, + onChange, +}: Props) { + const [query, setQuery] = useState(""); + + const timezones = moment.tz.names(); + + const filteredTimezones = query === "" ? timezones : timezones.filter((x) => x.toLowerCase().includes(query.toLowerCase())); + return ( + <> + +
+
+ setQuery(e.target.value)} + /> + + + +
+ setQuery("")} + > + + {filteredTimezones.map((timezone: string) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? "bg-mti-purple-light text-white" + : "text-gray-900" + }` + } + > + {timezone} + + ))} + + +
+
+ + ); +} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 4a390a3f..dec052ef 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -78,6 +78,7 @@ export interface DemographicInformation { gender: Gender; employment: EmploymentStatus; passport_id?: string; + timezone?: string; } export interface DemographicCorporateInformation { @@ -85,6 +86,7 @@ export interface DemographicCorporateInformation { phone: string; gender: Gender; position: string; + timezone?: string; } export type Gender = "male" | "female" | "other"; diff --git a/src/pages/api/assignments/[id]/export.tsx b/src/pages/api/assignments/[id]/export.tsx index b0a7755e..8bc10d4b 100644 --- a/src/pages/api/assignments/[id]/export.tsx +++ b/src/pages/api/assignments/[id]/export.tsx @@ -28,6 +28,7 @@ import { getRadialProgressPNG, streamToBuffer, } from "@/utils/pdf"; +import moment from "moment-timezone"; interface GroupScoreSummaryHelper { score: [number, number]; @@ -350,7 +351,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const pdfStream = await ReactPDF.renderToStream( { const user = req.session.user; @@ -83,7 +82,7 @@ function UserProfile({user, mutateUser}: Props) { const [commercialRegistration, setCommercialRegistration] = useState( user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); - + const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || 'UTC'); const {groups} = useGroups(); const {users} = useUsers(); @@ -146,6 +145,7 @@ function UserProfile({user, mutateUser}: Props) { position: user?.type === "corporate" ? position : undefined, gender, passport_id, + timezone, }, ...(user.type === "corporate" ? {corporateInformation} : {}), }); @@ -247,6 +247,13 @@ function UserProfile({user, mutateUser}: Props) {
); + const TimezoneInput = () => ( +
+ + +
+ ); + return (
@@ -304,6 +311,9 @@ function UserProfile({user, mutateUser}: Props) { + + + diff --git a/yarn.lock b/yarn.lock index 8646fe4a..29859e31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4360,6 +4360,13 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment-timezone@^0.5.44: + version "0.5.44" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.44.tgz#a64a4e47b68a43deeab5ae4eb4f82da77cdf595f" + integrity sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw== + dependencies: + moment "^2.29.4" + moment@^2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" From 4448c2019e94c93a032d9a438a7893db1aa54cbf Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 16 Jan 2024 18:48:01 +0000 Subject: [PATCH 27/37] Added some bold text to PDF footer --- src/exams/pdf/styles.ts | 3 +++ src/exams/pdf/test.report.footer.tsx | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/exams/pdf/styles.ts b/src/exams/pdf/styles.ts index 0440c37f..0a0d7e4b 100644 --- a/src/exams/pdf/styles.ts +++ b/src/exams/pdf/styles.ts @@ -28,6 +28,9 @@ export const styles = StyleSheet.create({ fontFamily: "Helvetica-Bold", fontWeight: "bold", }, + textNormal: { + fontWeight: "normal", + }, textColor: { color: "#4e4969", }, diff --git a/src/exams/pdf/test.report.footer.tsx b/src/exams/pdf/test.report.footer.tsx index 884ad2f0..3771fee7 100644 --- a/src/exams/pdf/test.report.footer.tsx +++ b/src/exams/pdf/test.report.footer.tsx @@ -18,18 +18,18 @@ const TestReportFooter = () => ( > - Validity + Validity This report remains valid for a duration of three months from the test date. - Confidential – circulated for concern people + Confidential – circulated for concern people - Declaration + Declaration We hereby declare that exam results on our platform, assessed by AI, are not the sole determinants of candidates' English proficiency From 31d3232f19ab7b77807cd8e03172458b424bf190 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 16 Jan 2024 19:24:19 +0000 Subject: [PATCH 28/37] Added passport id to PDF --- src/exams/pdf/group.test.report.tsx | 3 +++ src/exams/pdf/test.report.tsx | 3 +++ src/pages/api/assignments/[id]/export.tsx | 7 ++++--- src/pages/api/stats/[id]/export.tsx | 6 ++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/exams/pdf/group.test.report.tsx b/src/exams/pdf/group.test.report.tsx index 4b865494..24d25ca0 100644 --- a/src/exams/pdf/group.test.report.tsx +++ b/src/exams/pdf/group.test.report.tsx @@ -33,6 +33,7 @@ interface Props { summaryPNG: string; summaryScore: string; groupScoreSummary: any[]; + passportId: string; } const customStyles = StyleSheet.create({ @@ -81,6 +82,7 @@ const GroupTestReport = ({ summaryPNG, summaryScore, groupScoreSummary, + passportId, }: Props) => { const defaultTextStyle = [styles.textFont, { fontSize: 8 }]; return ( @@ -114,6 +116,7 @@ const GroupTestReport = ({ ID: {id} Email: {email} Gender: {gender} + Passport ID: {passportId} Total Number of Students: {numberOfStudents} diff --git a/src/exams/pdf/test.report.tsx b/src/exams/pdf/test.report.tsx index 5b72feaf..9c409554 100644 --- a/src/exams/pdf/test.report.tsx +++ b/src/exams/pdf/test.report.tsx @@ -27,6 +27,7 @@ interface Props { title: string; summaryPNG: string; summaryScore: string; + passportId: string; } const TestReport = ({ @@ -43,6 +44,7 @@ const TestReport = ({ renderDetails, summaryPNG, summaryScore, + passportId, }: Props) => { const defaultTextStyle = [styles.textFont, { fontSize: 8 }]; const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }]; @@ -83,6 +85,7 @@ const TestReport = ({ ID: {id} Email: {email} Gender: {gender} + Passport ID: {passportId} ); diff --git a/src/pages/api/stats/[id]/export.tsx b/src/pages/api/stats/[id]/export.tsx index b7a103db..0ff4d5a9 100644 --- a/src/pages/api/stats/[id]/export.tsx +++ b/src/pages/api/stats/[id]/export.tsx @@ -15,7 +15,7 @@ import { sessionOptions } from "@/lib/session"; import ReactPDF from "@react-pdf/renderer"; import TestReport from "@/exams/pdf/test.report"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; -import { User } from "@/interfaces/user"; +import { DemographicInformation, User } from "@/interfaces/user"; import { Module } from "@/interfaces"; import { ModuleScore } from "@/interfaces/module.scores"; import { SkillExamDetails } from "@/exams/pdf/details/skill.exam"; @@ -304,6 +304,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const { title, details } = getCustomData(); + const demographicInformation = user.demographicInformation as DemographicInformation; const pdfStream = await ReactPDF.renderToStream( ); From 8002c71b9122861e4d77c134baa40780d2a06c65 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 16 Jan 2024 22:22:55 +0000 Subject: [PATCH 29/37] Fixed issue with 100% being hyphenized --- src/exams/pdf/group.test.report.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exams/pdf/group.test.report.tsx b/src/exams/pdf/group.test.report.tsx index 24d25ca0..6d98cd8a 100644 --- a/src/exams/pdf/group.test.report.tsx +++ b/src/exams/pdf/group.test.report.tsx @@ -206,7 +206,7 @@ const GroupTestReport = ({ percentage={percent} /> - + {percent}% {description} From 6bcc303b740e685c7c20414bd9f4a49e2ffa3cd2 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 16 Jan 2024 22:24:08 +0000 Subject: [PATCH 30/37] Fixed institution print --- src/pages/api/assignments/[id]/export.tsx | 74 +++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/src/pages/api/assignments/[id]/export.tsx b/src/pages/api/assignments/[id]/export.tsx index 4a3a3e52..cfbbcc58 100644 --- a/src/pages/api/assignments/[id]/export.tsx +++ b/src/pages/api/assignments/[id]/export.tsx @@ -16,7 +16,7 @@ import { sessionOptions } from "@/lib/session"; import ReactPDF from "@react-pdf/renderer"; import GroupTestReport from "@/exams/pdf/group.test.report"; import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; -import { Stat } from "@/interfaces/user"; +import { Stat, CorporateUser } from "@/interfaces/user"; import { User, DemographicInformation } from "@/interfaces/user"; import { Module } from "@/interfaces"; import { ModuleScore, StudentData } from "@/interfaces/module.scores"; @@ -28,6 +28,7 @@ import { getRadialProgressPNG, streamToBuffer, } from "@/utils/pdf"; +import { Group } from "@/interfaces/user"; interface GroupScoreSummaryHelper { score: [number, number]; @@ -345,8 +346,73 @@ async function post(req: NextApiRequest, res: NextApiResponse) { return result; }; + const getInstitution = async () => { + try { + // due to database inconsistencies, I'll be overprotective here + const assignerUserSnap = await getDoc( + doc(db, "users", data.assigner) + ); + if (assignerUserSnap.exists()) { + // we'll need the user in order to get the user data (name, email, focus, etc); + const assignerUser = assignerUserSnap.data() as User; + + if (assignerUser.type === "teacher") { + // also search for groups where this user belongs + const queryGroups = query( + collection(db, "groups"), + where("participants", "array-contains", assignerUser.id) + ); + const groupSnapshot = await getDocs(queryGroups); + + const groups = groupSnapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; + + if (groups.length > 0) { + const adminQuery = query( + collection(db, "users"), + where( + documentId(), + "in", + groups.map((g) => g.admin) + ) + ); + const adminUsersSnap = await getDocs(adminQuery); + + const admins = adminUsersSnap.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as CorporateUser[]; + + const adminData = admins.find( + (a) => a.corporateInformation?.companyInformation?.name + ); + if (adminData) { + return adminData.corporateInformation.companyInformation + .name; + } + } + } + + if ( + assignerUser.type === "corporate" && + assignerUser.corporateInformation?.companyInformation?.name + ) { + return assignerUser.corporateInformation.companyInformation + .name; + } + } + } catch (err) { + console.error(err); + } + return ""; + }; + + const institution = await getInstitution(); const groupScoreSummary = getGroupScoreSummary(); - const demographicInformation = user.demographicInformation as DemographicInformation; + const demographicInformation = + user.demographicInformation as DemographicInformation; const pdfStream = await ReactPDF.renderToStream( ); From f85a1f5601798ab3ec8a76a50a9ea1aa60445308 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 16 Jan 2024 23:00:58 +0000 Subject: [PATCH 31/37] Updated part of the correction display for Writing --- src/components/Solutions/Writing.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/Solutions/Writing.tsx b/src/components/Solutions/Writing.tsx index fad9ff5a..9c5f39f9 100644 --- a/src/components/Solutions/Writing.tsx +++ b/src/components/Solutions/Writing.tsx @@ -12,19 +12,26 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on const [isModalOpen, setIsModalOpen] = useState(false); const formatSolution = (solution: string, errors: {correction: string | null; misspelled: string}[]) => { - const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|")); + const misspelled = errors.map((x) => x.misspelled); + console.log({misspelled}); + const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|"), "g"); + console.log(errorRegex.global); return ( <> - {reactStringReplace(solution, errorRegex, (match) => { - const correction = errors.find((x) => x.misspelled === match)?.correction; + {solution.split(" ").map((word) => { + if (!misspelled.includes(word)) return <>{word} ; + const correction = errors.find((x) => x.misspelled === word)?.correction; return ( - - {match} - + <> + + {word} + {" "} + ); })} From 5540e4a3e6b0a81d5dbd77f6f2fbc9be48407413 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Tue, 16 Jan 2024 23:11:16 +0000 Subject: [PATCH 32/37] Updated the profile page a bit to accommodate recent changes --- src/pages/profile.tsx | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 52a20db3..12806fbb 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -82,7 +82,7 @@ function UserProfile({user, mutateUser}: Props) { const [commercialRegistration, setCommercialRegistration] = useState( user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); - const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || 'UTC'); + const [timezone, setTimezone] = useState(user.demographicInformation?.timezone || "UTC"); const {groups} = useGroups(); const {users} = useUsers(); @@ -248,7 +248,7 @@ function UserProfile({user, mutateUser}: Props) { ); const TimezoneInput = () => ( -
+
@@ -293,27 +293,29 @@ function UserProfile({user, mutateUser}: Props) { /> - - {user.type === "student" && ( - setPassportID(e)} - placeholder="Enter National ID or Passport number" - value={passport_id} - required - /> - )} {user.type === "agent" && } - + + {user.type === "student" ? ( + + setPassportID(e)} + placeholder="Enter National ID or Passport number" + value={passport_id} + required + /> + + + ) : ( - + )} From 9ee09c8fda2aa71a28c0fb9b55d5b7eaac02dcfb Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 17 Jan 2024 11:22:23 +0000 Subject: [PATCH 33/37] Added a diff viewer for the writing correction --- package.json | 1 + src/components/Solutions/Writing.tsx | 85 ++++++++------- src/components/Solutions/index.tsx | 24 +++-- src/interfaces/exam.ts | 1 + yarn.lock | 156 ++++++++++++++++++++++++++- 5 files changed, 217 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index c7cb23e0..61673f4e 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-csv": "^2.2.2", "react-currency-input-field": "^3.6.12", "react-datepicker": "^4.18.0", + "react-diff-viewer": "^3.1.1", "react-dom": "18.2.0", "react-firebase-hooks": "^5.1.1", "react-icons": "^4.8.0", diff --git a/src/components/Solutions/Writing.tsx b/src/components/Solutions/Writing.tsx index 9c5f39f9..ea082e0d 100644 --- a/src/components/Solutions/Writing.tsx +++ b/src/components/Solutions/Writing.tsx @@ -6,37 +6,11 @@ import Button from "../Low/Button"; import {Dialog, Tab, Transition} from "@headlessui/react"; import {writingReverseMarking} from "@/utils/score"; import clsx from "clsx"; -import reactStringReplace from "react-string-replace"; +import ReactDiffViewer, {DiffMethod} from "react-diff-viewer"; export default function Writing({id, type, prompt, attachment, userSolutions, onNext, onBack}: WritingExercise & CommonProps) { const [isModalOpen, setIsModalOpen] = useState(false); - - const formatSolution = (solution: string, errors: {correction: string | null; misspelled: string}[]) => { - const misspelled = errors.map((x) => x.misspelled); - console.log({misspelled}); - const errorRegex = new RegExp(errors.map((x) => `(${x.misspelled})`).join("|"), "g"); - console.log(errorRegex.global); - - return ( - <> - {solution.split(" ").map((word) => { - if (!misspelled.includes(word)) return <>{word} ; - - const correction = errors.find((x) => x.misspelled === word)?.correction; - return ( - <> - - {word} - {" "} - - ); - })} - - ); - }; + const [showDiff, setShowDiff] = useState(false); return ( <> @@ -93,16 +67,51 @@ export default function Writing({id, type, prompt, attachment, userSolutions, on
{userSolutions && userSolutions.length > 0 && ( -
- Your answer: -
- {userSolutions[0]!.evaluation && userSolutions[0]!.evaluation.misspelled_pairs - ? formatSolution( - userSolutions[0]!.solution.replaceAll("\\n", "\n"), - userSolutions[0]!.evaluation.misspelled_pairs, - ) - : userSolutions[0]!.solution.replaceAll("\\n", "\n")} -
+
+ {!showDiff && ( + <> + Your answer: +
+ {userSolutions[0]!.solution.replaceAll("\\n", "\n")} +
+ + )} + {showDiff && ( + <> + Correction: +
+ +
+ + )} + {userSolutions[0].solution && userSolutions[0].evaluation?.fixed_text && ( + + )}
)} {userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && ( diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx index d61dc449..2926703a 100644 --- a/src/components/Solutions/index.tsx +++ b/src/components/Solutions/index.tsx @@ -30,20 +30,28 @@ export interface CommonProps { export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void) => { switch (exercise.type) { case "fillBlanks": - return ; + return ; case "trueFalse": - return ; + return ; case "matchSentences": - return ; + return ; case "multipleChoice": - return ; + return ( + + ); case "writeBlanks": - return ; + return ; case "writing": - return ; + return ; case "speaking": - return ; + return ; case "interactiveSpeaking": - return ; + return ; } }; diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index b1b19943..bd4802c7 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -110,6 +110,7 @@ interface InteractiveSpeakingEvaluation extends Evaluation { interface CommonEvaluation extends Evaluation { perfect_answer?: string; perfect_answer_1?: string; + fixed_text?: string; } export interface WritingExercise { diff --git a/yarn.lock b/yarn.lock index 29859e31..f262d9df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/helper-module-imports@^7.16.7": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.16.7": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== @@ -62,6 +62,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.7.2": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/types@^7.22.15": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" @@ -98,6 +105,16 @@ source-map "^0.5.7" stylis "4.2.0" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" @@ -109,6 +126,11 @@ "@emotion/weak-memoize" "^0.3.1" stylis "4.2.0" +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@emotion/hash@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" @@ -145,6 +167,17 @@ "@emotion/weak-memoize" "^0.3.1" hoist-non-react-statics "^3.3.1" +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/serialize@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" @@ -156,11 +189,26 @@ "@emotion/utils" "^1.2.1" csstype "^3.0.2" +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + "@emotion/sheet@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@emotion/unitless@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" @@ -171,11 +219,21 @@ resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + "@emotion/utils@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@emotion/weak-memoize@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" @@ -1837,6 +1895,31 @@ axobject-query@^3.1.1: dependencies: deep-equal "^2.0.5" +babel-plugin-emotion@^10.0.27: + version "10.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d" + integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + +babel-plugin-macros@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -1846,6 +1929,11 @@ babel-plugin-macros@^3.1.0: cosmiconfig "^7.0.0" resolve "^1.19.0" +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -2176,6 +2264,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -2202,6 +2301,16 @@ country-flag-icons@^1.5.4: resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.7.tgz#f1f2ddf14f3cbf01cba6746374aeba94db35d4b4" integrity sha512-AdvXhMcmSp7nBSkpGfW4qR/luAdRUutJqya9PuwRbsBzuoknThfultbv7Ib6fWsHXC43Es/4QJ8gzQQdBNm75A== +create-emotion@^10.0.14, create-emotion@^10.0.27: + version "10.0.27" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503" + integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg== + dependencies: + "@emotion/cache" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -2247,6 +2356,11 @@ cssesc@^3.0.0: resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +csstype@^2.5.7: + version "2.6.21" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" + integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== + csstype@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" @@ -2378,6 +2492,11 @@ didyoumean@^1.2.2: resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dijkstrajs@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" @@ -2476,6 +2595,14 @@ emoji-regex@^9.2.2: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +emotion@^10.0.14: + version "10.0.27" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e" + integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g== + dependencies: + babel-plugin-emotion "^10.0.27" + create-emotion "^10.0.27" + encode-utf8@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" @@ -3570,7 +3697,7 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -4252,7 +4379,7 @@ media-engine@^1.0.3: resolved "https://registry.yarnpkg.com/media-engine/-/media-engine-1.0.3.tgz#be3188f6cd243ea2a40804a35de5a5b032f58dad" integrity sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg== -memoize-one@^5.1.1: +memoize-one@^5.0.4, memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -5031,6 +5158,18 @@ react-datepicker@^4.18.0: react-onclickoutside "^6.13.0" react-popper "^2.3.0" +react-diff-viewer@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz#21ac9c891193d05a3734bfd6bd54b107ee6d46cc" + integrity sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw== + dependencies: + classnames "^2.2.6" + create-emotion "^10.0.14" + diff "^4.0.1" + emotion "^10.0.14" + memoize-one "^5.0.4" + prop-types "^15.6.2" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" @@ -5268,6 +5407,15 @@ resolve@^1.1.7, resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.12.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.19.0: version "1.22.6" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" @@ -6207,7 +6355,7 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0, yaml@^1.10.2: +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== From c26ff48b601898371dec74c2bd5585d8fd4bf205 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 17 Jan 2024 11:32:20 +0000 Subject: [PATCH 34/37] Solved some issues with the Student Dashboard --- src/dashboards/Student.tsx | 4 ++-- src/dashboards/Teacher.tsx | 2 +- src/pages/(admin)/BatchCodeGenerator.tsx | 2 +- src/utils/groups.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index 97acd6d1..f55bf16e 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -42,7 +42,7 @@ export default function StudentDashboard({user}: Props) { const setAssignment = useExamStore((state) => state.setAssignment); useEffect(() => { - getUserCorporate(user.id).then(setCorporateUserToShow); + getUserCorporate("IXdh9EQziAVXXh0jOiC5cPVlgS82").then(setCorporateUserToShow); }, [user]); const startAssignment = (assignment: Assignment) => { @@ -70,7 +70,7 @@ export default function StudentDashboard({user}: Props) { <> {corporateUserToShow && (
- Linked to: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name} + Linked to: {corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}
)} {corporateUserToShow && (
- Linked to: {corporateUserToShow?.corporateInformation.companyInformation.name || corporateUserToShow.name} + Linked to: {corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name}
)}
u.email).includes(email) ? { email: email.toString(), - name: `${firstName} ${lastName}`, + name: `${firstName ?? ""} ${lastName ?? ""}`.trim(), passport_id: passport_id.toString(), } : undefined; diff --git a/src/utils/groups.ts b/src/utils/groups.ts index 0d0dec55..44f63fe7 100644 --- a/src/utils/groups.ts +++ b/src/utils/groups.ts @@ -14,5 +14,5 @@ export const getUserCorporate = async (userID: string) => { const users = (await axios.get("/api/users/list")).data; const admins = groups.map((g) => users.find((u) => u.id === g.admin)); - return admins.map((x) => x?.type).includes("corporate") ? (admins[0] as CorporateUser) : undefined; + return admins.filter((x) => x?.type === "admin") ? (admins[0] as CorporateUser) : undefined; }; From 7a577a7ca2549e1f76cbc1f22be514ceb5f15625 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 17 Jan 2024 11:50:50 +0000 Subject: [PATCH 35/37] Solved another stupid bug --- src/utils/groups.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/groups.ts b/src/utils/groups.ts index 44f63fe7..5b7c894d 100644 --- a/src/utils/groups.ts +++ b/src/utils/groups.ts @@ -13,6 +13,6 @@ export const getUserCorporate = async (userID: string) => { const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; const users = (await axios.get("/api/users/list")).data; - const admins = groups.map((g) => users.find((u) => u.id === g.admin)); - return admins.filter((x) => x?.type === "admin") ? (admins[0] as CorporateUser) : undefined; + const admins = groups.map((g) => users.find((u) => u.id === g.admin)).filter((x) => x?.type === "corporate"); + return admins.length > 0 ? (admins[0] as CorporateUser) : undefined; }; From a646955493d6aaff2f0c6532b58927408c93ae01 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 17 Jan 2024 11:59:40 +0000 Subject: [PATCH 36/37] Solved a bug with calculations of the stats page --- src/pages/stats.tsx | 231 +++++++++----------------------------------- src/utils/score.ts | 25 ++--- 2 files changed, 60 insertions(+), 196 deletions(-) diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index a6abe9ef..21661251 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -139,9 +139,9 @@ export default function Stats() { } }, [startDate, endDate]); - const calculateTotalScore = (stats: Stat[]) => { + const calculateTotalScore = (stats: Stat[], divisionFactor: number) => { const moduleScores = calculateModuleScore(stats); - return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; + return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / divisionFactor; }; const calculateScorePerModule = (stats: Stat[], module: Module) => { @@ -278,7 +278,10 @@ export default function Stats() { Level{" "} - {calculateTotalScore(stats.filter((s) => timestampToMoment(s).isBefore(date))).toFixed(1)} + {calculateTotalScore( + stats.filter((s) => timestampToMoment(s).isBefore(date)), + 5, + ).toFixed(1)}
) : null; @@ -364,6 +367,7 @@ export default function Stats() { return date.isValid() ? calculateTotalScore( stats.filter((s) => timestampToMoment(s).isBefore(date)), + 5, ).toFixed(1) : undefined; }) @@ -599,188 +603,47 @@ export default function Stats() { }} />
- {/* Reading Score Band in Interval */} -
- Reading Score Band in Interval - ( +
+ {capitalize(module)} Score Band in Interval + moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Reading", - fill: false, - borderColor: COLORS[0], - backgroundColor: COLORS[0], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "reading", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Listening Score Band in Interval */} -
- Listening Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Listening", - fill: false, - borderColor: COLORS[1], - backgroundColor: COLORS[1], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "listening", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Writing Score Band in Interval */} -
- Writing Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Writing", - fill: false, - borderColor: COLORS[2], - backgroundColor: COLORS[2], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "writing", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Speaking Score Band in Interval */} -
- Speaking Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Speaking", - fill: false, - borderColor: COLORS[3], - backgroundColor: COLORS[3], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "speaking", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Level Score Band in Interval */} -
- Level Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Level", - fill: false, - borderColor: COLORS[4], - backgroundColor: COLORS[4], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter((s) => timestampToMoment(s).isBefore(date) && s.module === "level"), - ).toFixed(1); - }), - }, - ], - }} - /> -
+ }} + type="line" + data={{ + labels: intervalDates.map((date) => moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: capitalize(module), + fill: false, + borderColor: COLORS[index], + backgroundColor: COLORS[index], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter( + (s) => timestampToMoment(s).isBefore(date) && s.module === module, + ), + 1, + ).toFixed(1); + }), + }, + ], + }} + /> +
+ ))}
diff --git a/src/utils/score.ts b/src/utils/score.ts index 08707312..2e191025 100644 --- a/src/utils/score.ts +++ b/src/utils/score.ts @@ -1,5 +1,5 @@ import {Module} from "@/interfaces"; -import { LevelScore } from "@/constants/ielts"; +import {LevelScore} from "@/constants/ielts"; type Type = "academic" | "general"; @@ -96,7 +96,7 @@ const academicMarking: {[key: number]: number} = { const levelMarking: {[key: number]: number} = { 88: 9, // Advanced - 64: 8 , // Upper-Intermediate + 64: 8, // Upper-Intermediate 52: 6, // Intermediate 32: 4, // Pre-Intermediate 16: 2, // Elementary @@ -142,23 +142,24 @@ export const calculateBandScore = (correct: number, total: number, module: Modul }; export const calculateAverageLevel = (levels: {[key in Module]: number}) => { - return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 4; + return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 5; }; export const getLevelScore = (level: number) => { - switch(level) { + switch (level) { case 0: - return ['Beginner', 'Low A1']; + return ["Beginner", "Low A1"]; case 2: - return ['Elementary', 'High A1/Low A2']; + return ["Elementary", "High A1/Low A2"]; case 4: - return ['Pre-Intermediate', 'High A2/Low B1']; + return ["Pre-Intermediate", "High A2/Low B1"]; case 6: - return ['Intermediate', 'High B1/Low B2']; + return ["Intermediate", "High B1/Low B2"]; case 8: - return ['Upper-Intermediate', 'High B2/Low C1']; + return ["Upper-Intermediate", "High B2/Low C1"]; case 9: - return ['Advanced', 'C1']; - default: return []; + return ["Advanced", "C1"]; + default: + return []; } -} \ No newline at end of file +}; From 63d2baf35fa169bbb844e93540b548161a5ef194 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 17 Jan 2024 14:25:37 +0000 Subject: [PATCH 37/37] Improved the overall redirection of the login page --- src/hooks/useUser.tsx | 6 +++--- src/pages/login.tsx | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx index df6e9d43..47b9a36e 100644 --- a/src/hooks/useUser.tsx +++ b/src/hooks/useUser.tsx @@ -15,10 +15,10 @@ export default function useUser({redirectTo = "", redirectIfFound = false} = {}) if (!redirectTo || !user) return; if ( - // If redirectTo is set, redirect if the user was not found. - (redirectTo && !redirectIfFound && (!user || (user && !user.isVerified))) || // If redirectIfFound is also set, redirect if the user was found - (redirectIfFound && user && user.isVerified) + (redirectIfFound && user && user.isVerified) || + // If redirectTo is set, redirect if the user was not found. + (redirectTo && !redirectIfFound && (!user || (user && !user.isVerified))) ) { Router.push(redirectTo); } diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 357f9f6c..2f62981e 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -2,7 +2,7 @@ import {User} from "@/interfaces/user"; import {toast, ToastContainer} from "react-toastify"; import axios from "axios"; -import {FormEvent, useState} from "react"; +import {FormEvent, useEffect, useState} from "react"; import Head from "next/head"; import useUser from "@/hooks/useUser"; import {Divider} from "primereact/divider"; @@ -13,9 +13,38 @@ import Input from "@/components/Low/Input"; import clsx from "clsx"; import {useRouter} from "next/router"; import EmailVerification from "./(auth)/EmailVerification"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g); +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; + + const envVariables: {[key: string]: string} = {}; + Object.keys(process.env) + .filter((x) => x.startsWith("NEXT_PUBLIC")) + .forEach((x: string) => { + envVariables[x] = process.env[x]!; + }); + + if (user && user.isVerified) { + res.setHeader("location", "/"); + res.statusCode = 302; + res.end(); + return { + props: { + user: null, + envVariables, + }, + }; + } + + return { + props: {user: null, envVariables}, + }; +}, sessionOptions); + export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -29,6 +58,10 @@ export default function Login() { redirectIfFound: true, }); + useEffect(() => { + if (user && user.isVerified) router.push("/"); + }, [router, user]); + const forgotPassword = () => { if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) { toast.error("Please enter your e-mail to reset your password!", {toastId: "forgot-invalid-email"});