From f6bb69f994b6275de8330f673ccc14e4143c54d3 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 00:30:44 +0000 Subject: [PATCH 01/17] Updated the condition to close assignment: to be end date or when all students finish the assignment --- src/dashboards/Teacher.tsx | 5 ++-- src/exams/pdf/details/radial.result.tsx | 31 +++++++++------------ src/exams/pdf/details/skill.exam.tsx | 24 ++++++++--------- src/interfaces/module.scores.ts | 36 ++++++++++++------------- 4 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index e40d5a28..2d181089 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -151,8 +151,9 @@ export default function TeacherDashboard({user}: Props) { }; const AssignmentsPage = () => { - const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()); - const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()); + const activeFilter = (a: Assignment) => + moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; + const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length; const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); return ( diff --git a/src/exams/pdf/details/radial.result.tsx b/src/exams/pdf/details/radial.result.tsx index 125983a2..e5aa62db 100644 --- a/src/exams/pdf/details/radial.result.tsx +++ b/src/exams/pdf/details/radial.result.tsx @@ -1,24 +1,17 @@ /* eslint-disable jsx-a11y/alt-text */ import React from "react"; -import { View, Text, Image } from "@react-pdf/renderer"; -import { styles } from "../styles"; -import { ModuleScore } from "@/interfaces/module.scores"; +import {View, Text, Image} from "@react-pdf/renderer"; +import {styles} from "../styles"; +import {ModuleScore} from "@/interfaces/module.scores"; -export const RadialResult = ({ - module, - score, - total, - png, -}: ModuleScore) => ( - - - {module} - - - - {score} - out of {total} - - +export const RadialResult = ({module, score, total, png}: ModuleScore) => ( + + {module} + + + {score.toFixed(2)} + out of {total} + + ); diff --git a/src/exams/pdf/details/skill.exam.tsx b/src/exams/pdf/details/skill.exam.tsx index eb8a9581..459d4e3e 100644 --- a/src/exams/pdf/details/skill.exam.tsx +++ b/src/exams/pdf/details/skill.exam.tsx @@ -1,20 +1,20 @@ import React from "react"; -import { View, StyleSheet } from "@react-pdf/renderer"; -import { ModuleScore } from "@/interfaces/module.scores"; -import { RadialResult } from "./radial.result"; +import {View, StyleSheet} from "@react-pdf/renderer"; +import {ModuleScore} from "@/interfaces/module.scores"; +import {RadialResult} from "./radial.result"; interface Props { - testDetails: ModuleScore[]; + testDetails: ModuleScore[]; } const customStyles = StyleSheet.create({ - container: { display: "flex", flexDirection: "row", gap: 30 }, + container: {display: "flex", flexDirection: "row", gap: 30}, }); -export const SkillExamDetails = ({ testDetails }: Props) => ( - - {testDetails.map((detail) => { - const { module } = detail; - return ; - })} - +export const SkillExamDetails = ({testDetails}: Props) => ( + + {testDetails.map((detail) => { + const {module} = detail; + return ; + })} + ); diff --git a/src/interfaces/module.scores.ts b/src/interfaces/module.scores.ts index 2a4d4149..d52d51c4 100644 --- a/src/interfaces/module.scores.ts +++ b/src/interfaces/module.scores.ts @@ -1,22 +1,22 @@ import {Module} from "@/interfaces"; export interface ModuleScore { - score: number; - total: number; - code: Module; - module: Module | 'Overall'; - png?: string, - evaluation?: string, - suggestions?: string, - } + score: number; + total: number; + code: Module; + module: Module | "Overall"; + png?: string; + evaluation?: string; + suggestions?: string; +} - export interface StudentData { - id: string; - name: string; - email: string; - gender: string; - date: string; - result: string; - level?: string; - bandScore: number; - } \ No newline at end of file +export interface StudentData { + id: string; + name: string; + email: string; + gender: string; + date: string; + result: string; + level?: string; + bandScore: number; +} From 83b8ab7774f4fc15e28222453b85fde57eb5c1a4 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 12:48:29 +0000 Subject: [PATCH 02/17] Allowed admins and others to download reports related to other users --- src/pages/api/stats/[id]/export.tsx | 559 +++++++++++++--------------- 1 file changed, 257 insertions(+), 302 deletions(-) diff --git a/src/pages/api/stats/[id]/export.tsx b/src/pages/api/stats/[id]/export.tsx index 6ab6a217..b3374c4c 100644 --- a/src/pages/api/stats/[id]/export.tsx +++ b/src/pages/api/stats/[id]/export.tsx @@ -1,33 +1,20 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { app, storage } from "@/firebase"; -import { - getFirestore, - doc, - getDoc, - updateDoc, - getDocs, - query, - collection, - where, -} from "firebase/firestore"; -import { withIronSessionApiRoute } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; +import type {NextApiRequest, NextApiResponse} from "next"; +import {app, storage} from "@/firebase"; +import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where} from "firebase/firestore"; +import {withIronSessionApiRoute} from "iron-session/next"; +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 { DemographicInformation, User } from "@/interfaces/user"; -import { Module } from "@/interfaces"; -import { ModuleScore } from "@/interfaces/module.scores"; -import { SkillExamDetails } from "@/exams/pdf/details/skill.exam"; -import { LevelExamDetails } from "@/exams/pdf/details/level.exam"; -import { calculateBandScore } from "@/utils/score"; +import {ref, uploadBytes, getDownloadURL} from "firebase/storage"; +import {DemographicInformation, User} from "@/interfaces/user"; +import {Module} from "@/interfaces"; +import {ModuleScore} from "@/interfaces/module.scores"; +import {SkillExamDetails} from "@/exams/pdf/details/skill.exam"; +import {LevelExamDetails} from "@/exams/pdf/details/level.exam"; +import {calculateBandScore} from "@/utils/score"; import axios from "axios"; -import { moduleLabels } from "@/utils/moduleUtils"; -import { - generateQRCode, - getRadialProgressPNG, - streamToBuffer, -} from "@/utils/pdf"; +import {moduleLabels} from "@/utils/moduleUtils"; +import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf"; import moment from "moment-timezone"; const db = getFirestore(app); @@ -35,350 +22,318 @@ const db = getFirestore(app); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method === "GET") return get(req, res); - if (req.method === "POST") return post(req, res); + if (req.method === "GET") return get(req, res); + if (req.method === "POST") return post(req, res); } const getExamSummary = (score: number) => { - if (score > 0.8) { - return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language."; - } + if (score > 0.8) { + return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language."; + } - if (score > 0.6) { - return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery."; - } + if (score > 0.6) { + return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery."; + } - if (score > 0.4) { - return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended."; - } + if (score > 0.4) { + return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended."; + } - if (score > 0.2) { - return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency."; - } + if (score > 0.2) { + return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency."; + } - return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress."; + return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress."; }; const getLevelSummary = (score: number) => { - if (score > 0.8) { - return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability."; - } + if (score > 0.8) { + return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability."; + } - if (score > 0.6) { - return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies."; - } + if (score > 0.6) { + return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies."; + } - if (score > 0.4) { - return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies."; - } + if (score > 0.4) { + return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies."; + } - if (score > 0.2) { - return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency."; - } + if (score > 0.2) { + return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency."; + } - return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments."; + return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments."; }; const getPerformanceSummary = (module: Module, score: number) => { - if (module === "level") return getLevelSummary(score); - return getExamSummary(score); + if (module === "level") return getLevelSummary(score); + return getExamSummary(score); }; interface SkillsFeedbackRequest { - code: Module; - name: string; - grade: number; + code: Module; + name: string; + grade: number; } interface SkillsFeedbackResponse extends SkillsFeedbackRequest { - evaluation: string; - suggestions: string; + evaluation: string; + suggestions: string; } const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => { - const backendRequest = await axios.post( - `${process.env.BACKEND_URL}/grading_summary`, - { sections }, - { - headers: { - Authorization: `Bearer ${process.env.BACKEND_JWT}`, - }, - } - ); + const backendRequest = await axios.post( + `${process.env.BACKEND_URL}/grading_summary`, + {sections}, + { + headers: { + Authorization: `Bearer ${process.env.BACKEND_JWT}`, + }, + }, + ); - return backendRequest.data?.sections; + return backendRequest.data?.sections; }; // perform the request with several retries if needed -const handleSkillsFeedbackRequest = async ( - sections: SkillsFeedbackRequest[] -): Promise => { - let i = 0; - try { - const data = await getSkillsFeedback(sections); - return data; - } catch (err) { - if (i < 3) { - i++; - return handleSkillsFeedbackRequest(sections); - } +const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): Promise => { + let i = 0; + try { + const data = await getSkillsFeedback(sections); + return data; + } catch (err) { + if (i < 3) { + i++; + return handleSkillsFeedbackRequest(sections); + } - return null; - } + return null; + } }; async function post(req: NextApiRequest, res: NextApiResponse) { - // verify if it's a logged user that is trying to export - if (req.session.user) { - const { id } = req.query as { id: string }; - // fetch stats entries for this particular user with the requested exam session - const docsSnap = await getDocs( - query( - collection(db, "stats"), - where("session", "==", id), - where("user", "==", req.session.user.id) - ) - ); + // verify if it's a logged user that is trying to export + if (req.session.user) { + const {id} = req.query as {id: string}; + // fetch stats entries for this particular user with the requested exam session + const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id))); - if (docsSnap.empty) { - res.status(400).end(); - return; - } + if (docsSnap.empty) { + res.status(400).end(); + return; + } - const stats = docsSnap.docs.map((d) => d.data()); - // verify if the stats already have a pdf generated - const hasPDF = stats.find((s) => s.pdf); + const stats = docsSnap.docs.map((d) => d.data()); + // verify if the stats already have a pdf generated + const hasPDF = stats.find((s) => s.pdf); - if (hasPDF) { - // if it does, return the pdf url - const fileRef = ref(storage, hasPDF.pdf); - const url = await getDownloadURL(fileRef); + if (hasPDF) { + // if it does, return the pdf url + const fileRef = ref(storage, hasPDF.pdf); + const url = await getDownloadURL(fileRef); - res.status(200).end(url); - return; - } + res.status(200).end(url); + return; + } - try { - // generate the pdf report - const docUser = await getDoc(doc(db, "users", req.session.user.id)); + try { + // generate the pdf report + const docUser = await getDoc(doc(db, "users", req.session.user.id)); - if (docUser.exists()) { - // we'll need the user in order to get the user data (name, email, focus, etc); - const user = docUser.data() as User; + if (docUser.exists()) { + // we'll need the user in order to get the user data (name, email, focus, etc); + const user = docUser.data() as User; - // generate the QR code for the report - const qrcode = await generateQRCode( - (req.headers.origin || "") + req.url - ); + // generate the QR code for the report + const qrcode = await generateQRCode((req.headers.origin || "") + req.url); - if (!qrcode) { - res.status(500).json({ ok: false }); - return; - } + if (!qrcode) { + res.status(500).json({ok: false}); + return; + } - // stats may contain multiple exams of the same type so we need to aggregate them - const results = ( - stats.reduce((accm: ModuleScore[], { module, score }) => { - const fixedModuleStr = - module[0].toUpperCase() + module.substring(1); - if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) { - return accm.map((e: ModuleScore) => { - if (e.module === fixedModuleStr) { - return { - ...e, - score: e.score + score.correct, - total: e.total + score.total, - }; - } + // stats may contain multiple exams of the same type so we need to aggregate them + const results = ( + stats.reduce((accm: ModuleScore[], {module, score}) => { + const fixedModuleStr = module[0].toUpperCase() + module.substring(1); + if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) { + return accm.map((e: ModuleScore) => { + if (e.module === fixedModuleStr) { + return { + ...e, + score: e.score + score.correct, + total: e.total + score.total, + }; + } - return e; - }); - } + return e; + }); + } - return [ - ...accm, - { - module: fixedModuleStr, - score: score.correct, - total: score.total, - code: module, - }, - ]; - }, []) as ModuleScore[] - ).map((moduleScore) => { - const { score, total } = moduleScore; - // with all the scores aggreated we can calculate the band score for each module - const bandScore = calculateBandScore( - score, - total, - moduleScore.code as Module, - user.focus - ); + return [ + ...accm, + { + module: fixedModuleStr, + score: score.correct, + total: score.total, + code: module, + }, + ]; + }, []) as ModuleScore[] + ).map((moduleScore) => { + const {score, total} = moduleScore; + // with all the scores aggreated we can calculate the band score for each module + const bandScore = calculateBandScore(score, total, moduleScore.code as Module, user.focus); - return { - ...moduleScore, - // generate the closest radial progress png for the score - png: getRadialProgressPNG("azul", score, total), - bandScore, - }; - }); + return { + ...moduleScore, + // generate the closest radial progress png for the score + png: getRadialProgressPNG("azul", score, total), + bandScore, + }; + }); - // get the skills feedback from the backend based on the module grade - const skillsFeedback = (await handleSkillsFeedbackRequest( - results.map(({ code, bandScore }) => ({ - code, - name: moduleLabels[code], - grade: bandScore, - })) - )) as SkillsFeedbackResponse[]; + // get the skills feedback from the backend based on the module grade + const skillsFeedback = (await handleSkillsFeedbackRequest( + results.map(({code, bandScore}) => ({ + code, + name: moduleLabels[code], + grade: bandScore, + })), + )) as SkillsFeedbackResponse[]; - if (!skillsFeedback) { - res.status(500).json({ ok: false }); - return; - } + if (!skillsFeedback) { + res.status(500).json({ok: false}); + return; + } - // assign the feedback to the results - const finalResults = results.map((result) => { - const feedback = skillsFeedback.find( - (f: SkillsFeedbackResponse) => f.code === result.code - ); + // assign the feedback to the results + const finalResults = results.map((result) => { + const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code); - if (feedback) { - return { - ...result, - evaluation: feedback?.evaluation, - suggestions: feedback?.suggestions, - }; - } + if (feedback) { + return { + ...result, + evaluation: feedback?.evaluation, + suggestions: feedback?.suggestions, + }; + } - return result; - }); + return result; + }); - // calculate the overall score out of all the aggregated results - const overallScore = results.reduce( - (accm, { score }) => accm + score, - 0 - ); - const overallTotal = results.reduce( - (accm, { total }) => accm + total, - 0 - ); - const overallResult = overallScore / overallTotal; + // calculate the overall score out of all the aggregated results + const overallScore = results.reduce((accm, {score}) => accm + score, 0); + const overallTotal = results.reduce((accm, {total}) => accm + total, 0); + const overallResult = overallScore / overallTotal; - const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal); + const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal); - // generate the overall detail report - const overallDetail = { - module: "Overall", - score: overallScore, - total: overallTotal, - png: overallPNG, - } as ModuleScore; - const testDetails = [overallDetail, ...finalResults]; + // generate the overall detail report + const overallDetail = { + module: "Overall", + score: overallScore, + total: overallTotal, + png: overallPNG, + } as ModuleScore; + const testDetails = [overallDetail, ...finalResults]; - const [stat] = stats; + const [stat] = stats; - // generate the performance summary based on the overall result - const performanceSummary = getPerformanceSummary( - stat.module, - overallResult - ); + // generate the performance summary based on the overall result + const performanceSummary = getPerformanceSummary(stat.module, overallResult); - // level exams have a different report structure than the skill exams - const getCustomData = () => { - if (stat.module === "level") { - return { - title: "ENGLISH LEVEL TEST RESULT REPORT ", - details: ( - - ), - }; - } + // level exams have a different report structure than the skill exams + const getCustomData = () => { + if (stat.module === "level") { + return { + title: "ENGLISH LEVEL TEST RESULT REPORT ", + details: , + }; + } - return { - title: "ENGLISH SKILLS TEST RESULT REPORT", - details: , - }; - }; + return { + title: "ENGLISH SKILLS TEST RESULT REPORT", + details: , + }; + }; - const { title, details } = getCustomData(); - - const demographicInformation = user.demographicInformation as DemographicInformation; - const pdfStream = await ReactPDF.renderToStream( - - ); + const {title, details} = getCustomData(); - // generate the file ref for storage - const fileName = `${Date.now().toString()}.pdf`; - const refName = `exam_report/${fileName}`; - const fileRef = ref(storage, refName); + const demographicInformation = user.demographicInformation as DemographicInformation; + const pdfStream = await ReactPDF.renderToStream( + , + ); - // upload the pdf to storage - const pdfBuffer = await streamToBuffer(pdfStream); - const snapshot = await uploadBytes(fileRef, pdfBuffer, { - contentType: "application/pdf", - }); + // generate the file ref for storage + const fileName = `${Date.now().toString()}.pdf`; + const refName = `exam_report/${fileName}`; + const fileRef = ref(storage, refName); - // update the stats entries with the pdf url to prevent duplication - docsSnap.docs.forEach(async (doc) => { - await updateDoc(doc.ref, { - pdf: refName, - }); - }); - const url = await getDownloadURL(fileRef); - res.status(200).end(url); - return; - } + // upload the pdf to storage + const pdfBuffer = await streamToBuffer(pdfStream); + const snapshot = await uploadBytes(fileRef, pdfBuffer, { + contentType: "application/pdf", + }); - res.status(401).json({ ok: false }); - return; - } catch (err) { - res.status(500).json({ ok: false }); - return; - } - } + // update the stats entries with the pdf url to prevent duplication + docsSnap.docs.forEach(async (doc) => { + await updateDoc(doc.ref, { + pdf: refName, + }); + }); + const url = await getDownloadURL(fileRef); + res.status(200).end(url); + return; + } - res.status(401).json({ ok: false }); - return; + res.status(401).json({ok: false}); + return; + } catch (err) { + res.status(500).json({ok: false}); + return; + } + } + + res.status(401).json({ok: false}); + return; } async function get(req: NextApiRequest, res: NextApiResponse) { - const { id } = req.query as { id: string }; - const docsSnap = await getDocs( - query(collection(db, "stats"), where("session", "==", id)) - ); + const {id} = req.query as {id: string}; + const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id))); - if (docsSnap.empty) { - res.status(404).end(); - return; - } + if (docsSnap.empty) { + res.status(404).end(); + return; + } - const stats = docsSnap.docs.map((d) => d.data()); + const stats = docsSnap.docs.map((d) => d.data()); - const hasPDF = stats.find((s) => s.pdf); + const hasPDF = stats.find((s) => s.pdf); - if (hasPDF) { - const fileRef = ref(storage, hasPDF.pdf); - const url = await getDownloadURL(fileRef); - return res.redirect(url); - } + if (hasPDF) { + const fileRef = ref(storage, hasPDF.pdf); + const url = await getDownloadURL(fileRef); + return res.redirect(url); + } - res.status(500).end(); + res.status(500).end(); } From da93b79c78d9bfe448a8b0fe79e8e99ab27ee9c4 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 13:34:48 +0000 Subject: [PATCH 03/17] Solved an issue with sorting --- src/utils/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index 8c3650f2..24720d0b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,8 +2,8 @@ import moment from "moment"; export function dateSorter(a: any, b: any, direction: "asc" | "desc", key: string) { if (!a[key] && !b[key]) return 0; - if (a[key] && !b[key]) return direction === "asc" ? -1 : 1; - if (!a[key] && b[key]) return direction === "asc" ? 1 : -1; + if (a[key] && !b[key]) return direction === "asc" ? 1 : -1; + if (!a[key] && b[key]) return direction === "asc" ? -1 : 1; if (moment(a[key]).isAfter(b[key])) return direction === "asc" ? 1 : -1; if (moment(b[key]).isAfter(a[key])) return direction === "asc" ? -1 : 1; return 0; From c7f303e410a04a519a2e011903da0e1e2710d604 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 20:20:08 +0000 Subject: [PATCH 04/17] Solved a bug --- src/pages/stats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index 21661251..3d3f09a4 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -178,7 +178,7 @@ export default function Stats() { }, { icon: , - value: `${stats.length > 0 ? averageScore(userStats) : 0}%`, + value: `${userStats.length > 0 ? averageScore(userStats) : 0}%`, label: "Average Score", }, ]} From 52143d2472fcb5545a1c4d6d1ee50fa2270712c5 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 20:23:48 +0000 Subject: [PATCH 05/17] Solved a bug that caused some user's profile page to crash --- src/components/Low/CountrySelect.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Low/CountrySelect.tsx b/src/components/Low/CountrySelect.tsx index 838395da..b9a5a110 100644 --- a/src/components/Low/CountrySelect.tsx +++ b/src/components/Low/CountrySelect.tsx @@ -42,7 +42,9 @@ export default function CountrySelect({value, disabled = false, onChange}: Props displayValue={(code: string) => { const country = countries[code as unknown as keyof TCountries]; - return `${countryCodes.findOne("countryCode" as any, code).flag} ${country.name} (+${country.phone})`; + return `${countryCodes.findOne("countryCode" as any, code)?.flag || ""} ${country?.name || "N/A"} (+${ + country?.phone || "N/A" + })`; }} /> From 8c1da3a84a68993c0fd528477a217e1ef018ea6f Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 20:28:42 +0000 Subject: [PATCH 06/17] Updated the UserCard to only show buttons to adequate users --- src/components/UserCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 4c0c48d3..c3cd9bce 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -574,17 +574,17 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
- {onViewCorporate && ( + {onViewCorporate && ["student", "teacher"].includes(user.type) && ( )} - {onViewStudents && ( + {onViewStudents && ["corporate", "teacher"].includes(user.type) && ( )} - {onViewTeachers && ( + {onViewTeachers && ["student", "corporate"].includes(user.type) && ( From cfde8ac9f09c08e5750b8ab29da2f4eb0be4d0be Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 21 Jan 2024 20:35:35 +0000 Subject: [PATCH 07/17] Updated it so the Corporate is updated into Active when its payment is accepted --- src/pages/api/payments/[id].ts | 4 ++++ src/pages/api/payments/files/[type]/[paymentId].ts | 2 ++ src/pages/payment-record.tsx | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/api/payments/[id].ts b/src/pages/api/payments/[id].ts index e645fe91..9d5d1c9b 100644 --- a/src/pages/api/payments/[id].ts +++ b/src/pages/api/payments/[id].ts @@ -74,6 +74,10 @@ async function patch(req: NextApiRequest, res: NextApiResponse) { if (user.type === "admin" || user.type === "developer") { await setDoc(snapshot.ref, req.body, {merge: true}); + if (req.body.isPaid) { + const corporateID = req.body.corporate; + await setDoc(doc(db, "users", corporateID), {status: "active"}, {merge: true}); + } return res.status(200).json({ok: true}); } diff --git a/src/pages/api/payments/files/[type]/[paymentId].ts b/src/pages/api/payments/files/[type]/[paymentId].ts index 5da7720b..88f3d5d6 100644 --- a/src/pages/api/payments/files/[type]/[paymentId].ts +++ b/src/pages/api/payments/files/[type]/[paymentId].ts @@ -119,6 +119,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const updatedDoc = (await getDoc(doc(db, "payments", paymentId))).data() as Payment; if (updatedDoc.commissionTransfer && updatedDoc.corporateTransfer) { await setDoc(doc(db, "payments", paymentId), {isPaid: true}, {merge: true}); + + await setDoc(doc(db, "users", updatedDoc.corporate), {status: "active"}, {merge: true}); } res.status(200).json({ref}); } catch (error) { diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 8ccc28c3..f352d781 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -111,7 +111,7 @@ const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () = options={(users.filter((u) => u.type === "corporate") as CorporateUser[]).map((user) => ({ value: user.id, meta: user, - label: `${user.corporateInformation.companyInformation.name || user.name} - ${user.email}`, + label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`, }))} defaultValue={{value: "undefined", label: "Select an account"}} onChange={(value) => setCorporate((value as any)?.meta ?? undefined)} From c868ea8795a15dd01e861da5b3ee56dc7ae92d87 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 22 Jan 2024 14:18:08 +0000 Subject: [PATCH 08/17] Turned the nav to Gray when it is disabled --- src/components/Sidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b9da2b6c..19712a67 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -45,8 +45,9 @@ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false} From 81943dbf424030f8d2f854dd8047d21984a0cbf5 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 22 Jan 2024 18:50:12 +0000 Subject: [PATCH 09/17] Updated the module generation to allow for only certain parts to be made --- src/interfaces/exam.ts | 2 +- .../(generation)/ListeningGeneration.tsx | 15 +++++-- src/pages/(generation)/ReadingGeneration.tsx | 20 ++++++--- src/pages/(generation)/SpeakingGeneration.tsx | 14 ++++-- src/pages/(generation)/WritingGeneration.tsx | 44 ++++++++++++------- 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index ee08f0f5..b8e12632 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -1,7 +1,7 @@ import {Module} from "."; export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam; -export type Variant = "diagnostic" | "partial"; +export type Variant = "full" | "diagnostic" | "partial"; export interface ReadingExam { parts: ReadingPart[]; diff --git a/src/pages/(generation)/ListeningGeneration.tsx b/src/pages/(generation)/ListeningGeneration.tsx index bb74f6ca..c2d26237 100644 --- a/src/pages/(generation)/ListeningGeneration.tsx +++ b/src/pages/(generation)/ListeningGeneration.tsx @@ -110,6 +110,7 @@ const ListeningGeneration = () => { const [part2, setPart2] = useState(); const [part3, setPart3] = useState(); const [part4, setPart4] = useState(); + const [minTimer, setMinTimer] = useState(60); const [isLoading, setIsLoading] = useState(false); const [resultingExam, setResultingExam] = useState(); const [types, setTypes] = useState([]); @@ -129,12 +130,13 @@ const ListeningGeneration = () => { const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type])); const submitExam = () => { - if (!part1 || !part2 || !part3 || !part4) return toast.error("Please generate all for sections!"); + const parts = [part1, part2, part3, part4].filter((x) => !!x); + if (parts.length === 0) return toast.error("Please generate at least one section!"); setIsLoading(true); axios - .post(`/api/exam/listening/generate/listening`, {parts: [part1, part2, part3, part4]}) + .post(`/api/exam/listening/generate/listening`, {parts, minTimer}) .then((result) => { playSound("sent"); console.log(`Generated Exam ID: ${result.data.id}`); @@ -172,6 +174,11 @@ const ListeningGeneration = () => { return ( <> +
+ + setMinTimer(parseInt(e))} value={minTimer} className="max-w-[300px]" /> +
+
@@ -264,14 +271,14 @@ const ListeningGeneration = () => { )} )} )} )} +
{isLoading && (
@@ -83,6 +121,7 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se ))}
)} + {part.result && Video Generated: ✅}
)} @@ -94,6 +133,7 @@ interface SpeakingPart { question?: string; questions?: string[]; topic: string; + result?: SpeakingExercise | InteractiveSpeakingExercise; } const SpeakingGeneration = () => { @@ -115,19 +155,21 @@ const SpeakingGeneration = () => { const setSelectedModules = useExamStore((state) => state.setSelectedModules); const submitExam = () => { - if (!part1 && !part2 && !part3) return toast.error("Please generate at least one task!"); + if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!"); setIsLoading(true); + const exam: SpeakingExam = { + id: v4(), + isDiagnostic: false, + exercises: [part1?.result, part2?.result, part3?.result].filter((x) => !!x) as (SpeakingExercise | InteractiveSpeakingExercise)[], + minTimer, + variant: minTimer >= 14 ? "full" : "partial", + module: "speaking", + }; + axios - .post(`/api/exam/speaking/generate/speaking`, { - exercises: [ - {...part1, type: "1"}, - {...part2, type: "2"}, - {...part3, type: "3"}, - ].filter((x) => !!x), - minTimer, - }) + .post(`/api/exam/speaking`, exam) .then((result) => { playSound("sent"); console.log(`Generated Exam ID: ${result.data.id}`); @@ -137,10 +179,11 @@ const SpeakingGeneration = () => { setPart1(undefined); setPart2(undefined); setPart3(undefined); + setMinTimer(14); }) .catch((error) => { console.log(error); - toast.error("Something went wrong!"); + toast.error("Something went wrong while generating, please try again later."); }) .finally(() => setIsLoading(false)); }; @@ -185,7 +228,7 @@ const SpeakingGeneration = () => { selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking", ) }> - Task 1 {part1 && } + Exercise 1 {part1 && part1.result && } @@ -196,7 +239,7 @@ const SpeakingGeneration = () => { selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking", ) }> - Task 2 {part2 && } + Exercise 2 {part2 && part2.result && } @@ -207,7 +250,7 @@ const SpeakingGeneration = () => { selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-speaking", ) }> - Task 3 {part3 && } + Interactive {part3 && part3.result && } @@ -234,7 +277,7 @@ const SpeakingGeneration = () => { )}
)} - {part && ( + {part && !isLoading && (

{part.topic}

{part.question && {part.question}} From 710c7931aa70af19943b51d497d165a2fa140171 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 24 Jan 2024 15:40:05 +0000 Subject: [PATCH 17/17] Updated the permissions to disallow corporate and teachers from editing other users --- src/constants/userPermissions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants/userPermissions.ts b/src/constants/userPermissions.ts index e0251e8c..734e9267 100644 --- a/src/constants/userPermissions.ts +++ b/src/constants/userPermissions.ts @@ -18,8 +18,8 @@ export const PERMISSIONS = { developer: ["developer"], }, updateUser: { - student: ["teacher", "corporate", "developer", "admin"], - teacher: ["corporate", "developer", "admin"], + student: ["developer", "admin"], + teacher: ["developer", "admin"], corporate: ["admin", "developer"], admin: ["developer", "admin"], agent: ["developer", "admin"],