From 52d309e7f488344a5749315901a358877d10c24e Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Sat, 17 Feb 2024 10:43:55 +0000 Subject: [PATCH 1/5] Fixed NaN display on level progress --- src/dashboards/Student.tsx | 56 ++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index 28c0385c..c22915c0 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -227,35 +227,39 @@ export default function StudentDashboard({user}: Props) {
Score History
- {MODULE_ARRAY.map((module) => ( -
-
-
- {module === "reading" && } - {module === "listening" && } - {module === "writing" && } - {module === "speaking" && } - {module === "level" && } + {MODULE_ARRAY.map((module) => { + const desiredLevel = user.desiredLevels[module] || 9; + const level = user.levels[module] || 0; + return ( +
+
+
+ {module === "reading" && } + {module === "listening" && } + {module === "writing" && } + {module === "speaking" && } + {module === "level" && } +
+
+ {capitalize(module)} + + Level {level} / Level 9 (Desired Level: {desiredLevel}) + +
-
- {capitalize(module)} - - Level {user.levels[module] || 0} / Level 9 (Desired Level: {user.desiredLevels[module] || 9}) - +
+
-
- -
-
- ))} + ); + })}
From 04f97b62c38407c65911ba994d1b020c806c691b Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Sat, 17 Feb 2024 14:22:46 +0000 Subject: [PATCH 2/5] Filtered out level from students history display --- src/dashboards/Student.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index c22915c0..d84f35d2 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -227,7 +227,12 @@ export default function StudentDashboard({user}: Props) {
Score History
- {MODULE_ARRAY.map((module) => { + {MODULE_ARRAY + // filtered out level as the questions for a level test are registed as the other modules + // therefore there are no stats to display on the level section + // for future reference, this data is registered on /api/stats/update.ts:90 + .filter((module) => module !== 'level') + .map((module) => { const desiredLevel = user.desiredLevels[module] || 9; const level = user.levels[module] || 0; return ( From 29cae5c3d259a15847cd5fb0158b8eda71330732 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Sun, 18 Feb 2024 11:09:25 +0000 Subject: [PATCH 3/5] Stats for Level exam are now being properly calculated --- src/dashboards/Student.tsx | 4 ---- src/pages/api/stats/update.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/dashboards/Student.tsx b/src/dashboards/Student.tsx index d84f35d2..f3c6eca4 100644 --- a/src/dashboards/Student.tsx +++ b/src/dashboards/Student.tsx @@ -228,10 +228,6 @@ export default function StudentDashboard({user}: Props) { Score History
{MODULE_ARRAY - // filtered out level as the questions for a level test are registed as the other modules - // therefore there are no stats to display on the level section - // for future reference, this data is registered on /api/stats/update.ts:90 - .filter((module) => module !== 'level') .map((module) => { const desiredLevel = user.desiredLevels[module] || 9; const level = user.levels[module] || 0; diff --git a/src/pages/api/stats/update.ts b/src/pages/api/stats/update.ts index c1c01674..7d089445 100644 --- a/src/pages/api/stats/update.ts +++ b/src/pages/api/stats/update.ts @@ -5,6 +5,7 @@ import {Stat, User} from "@/interfaces/user"; import {sessionOptions} from "@/lib/session"; import {calculateBandScore} from "@/utils/score"; import {groupByModule, groupBySession} from "@/utils/stats"; +import { MODULE_ARRAY } from "@/utils/moduleUtils"; import {getAuth} from "firebase/auth"; import {collection, doc, getDoc, getDocs, getFirestore, query, updateDoc, where} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; @@ -55,7 +56,7 @@ async function update(req: NextApiRequest, res: NextApiResponse) { }, }; - MODULES.forEach((module: Module) => { + MODULE_ARRAY.forEach((module: Module) => { const moduleStats = sessionStats.filter((x) => x.module === module); if (moduleStats.length === 0) return; @@ -87,11 +88,18 @@ async function update(req: NextApiRequest, res: NextApiResponse) { .filter((x) => x.total > 0) .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + const levelLevel = sessionLevels + .map((x) => x.level) + .filter((x) => x.total > 0) + .reduce((acc, cur) => ({total: acc.total + cur.total, correct: acc.correct + cur.correct}), {total: 0, correct: 0}); + + const levels = { reading: calculateBandScore(readingLevel.correct, readingLevel.total, "reading", req.session.user.focus), listening: calculateBandScore(listeningLevel.correct, listeningLevel.total, "listening", req.session.user.focus), writing: calculateBandScore(writingLevel.correct, writingLevel.total, "writing", req.session.user.focus), speaking: calculateBandScore(speakingLevel.correct, speakingLevel.total, "speaking", req.session.user.focus), + level: calculateBandScore(levelLevel.correct, levelLevel.total, "level", req.session.user.focus), }; const userDoc = doc(db, "users", req.session.user.id); From cdfafb3eea2cc62cdbaac5625e8518d0e89fc5aa Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Sun, 18 Feb 2024 11:46:08 +0000 Subject: [PATCH 4/5] Added approach to archive past assignments --- src/dashboards/AssignmentCard.tsx | 23 ++++++++--- src/dashboards/Teacher.tsx | 9 ++--- src/hooks/useAssignmentArchive.tsx | 45 ++++++++++++++++++++++ src/interfaces/results.ts | 1 + src/pages/api/assignments/[id]/archive.tsx | 33 ++++++++++++++++ 5 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 src/hooks/useAssignmentArchive.tsx create mode 100644 src/pages/api/assignments/[id]/archive.tsx diff --git a/src/dashboards/AssignmentCard.tsx b/src/dashboards/AssignmentCard.tsx index 9744fdd8..f08ae396 100644 --- a/src/dashboards/AssignmentCard.tsx +++ b/src/dashboards/AssignmentCard.tsx @@ -13,11 +13,14 @@ import { BsPen, } from "react-icons/bs"; import { usePDFDownload } from "@/hooks/usePDFDownload"; +import { useAssignmentArchive } from "@/hooks/useAssignmentArchive"; import { uniqBy } from "lodash"; interface Props { onClick?: () => void; allowDownload?: boolean; + reload?: Function; + allowArchive?: boolean; } export default function AssignmentCard({ @@ -29,11 +32,14 @@ export default function AssignmentCard({ assignees, results, exams, + archived, onClick, allowDownload, + reload, + allowArchive, }: Assignment & Props) { - const { users } = useUsers(); const renderPdfIcon = usePDFDownload("assignments"); + const renderArchiveIcon = useAssignmentArchive(id, reload); const calculateAverageModuleScore = (module: Module) => { const resultModuleBandScores = results.map((r) => { @@ -41,11 +47,11 @@ export default function AssignmentCard({ const correct = moduleStats.reduce( (acc, curr) => acc + curr.score.correct, - 0, + 0 ); const total = moduleStats.reduce( (acc, curr) => acc + curr.score.total, - 0, + 0 ); return calculateBandScore(correct, total, module, r.type); }); @@ -64,8 +70,13 @@ export default function AssignmentCard({

{name}

- {allowDownload && - renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} +
+ {allowDownload && + renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} + {allowArchive && + !archived && + renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} +
{module === "reading" && } diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index 4535d724..e7f3036d 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -151,9 +151,8 @@ export default function TeacherDashboard({user}: Props) { }; const AssignmentsPage = () => { - 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 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) && !a.archived; const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); return ( @@ -235,7 +234,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 reload={reloadAssignments} allowArchive/> ))}
@@ -281,7 +280,7 @@ export default function TeacherDashboard({user}: Props) { Assignments - {assignments.length} + {assignments.filter((a) => !a.archived).length} diff --git a/src/hooks/useAssignmentArchive.tsx b/src/hooks/useAssignmentArchive.tsx new file mode 100644 index 00000000..67879189 --- /dev/null +++ b/src/hooks/useAssignmentArchive.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import axios from "axios"; +import { toast } from "react-toastify"; +import { BsArchive } from "react-icons/bs"; + +export const useAssignmentArchive = ( + assignmentId: string, + reload?: Function +) => { + const [loading, setLoading] = React.useState(false); + const archive = () => { + // archive assignment + setLoading(true); + axios + .post(`/api/assignments/${assignmentId}/archive`) + .then((res) => { + toast.success("Assignment archived!"); + if(reload) reload(); + setLoading(false); + }) + .catch((err) => { + toast.error("Failed to archive the assignment!"); + setLoading(false); + }); + }; + + const renderIcon = (downloadClasses: string, loadingClasses: string) => { + if (loading) { + return ( + + ); + } + return ( + { + e.stopPropagation(); + archive(); + }} + /> + ); + }; + + return renderIcon; +}; diff --git a/src/interfaces/results.ts b/src/interfaces/results.ts index df04ef3c..1f4f4685 100644 --- a/src/interfaces/results.ts +++ b/src/interfaces/results.ts @@ -24,4 +24,5 @@ export interface Assignment { instructorGender?: InstructorGender; startDate: Date; endDate: Date; + archived?: boolean; } diff --git a/src/pages/api/assignments/[id]/archive.tsx b/src/pages/api/assignments/[id]/archive.tsx new file mode 100644 index 00000000..b7e27c94 --- /dev/null +++ b/src/pages/api/assignments/[id]/archive.tsx @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { app } from "@/firebase"; +import { getFirestore, doc, getDoc, setDoc } from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function post(req: NextApiRequest, res: NextApiResponse) { + // verify if it's a logged user that is trying to archive + if (req.session.user) { + const { id } = req.query as { id: string }; + const docSnap = await getDoc(doc(db, "assignments", id)); + + if (!docSnap.exists()) { + res.status(404).json({ ok: false }); + return; + } + + await setDoc(docSnap.ref, { archived: true }, { merge: true }); + res.status(200).json({ ok: true }); + return; + } + + res.status(401).json({ ok: false }); +} + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "POST") return post(req, res); + res.status(404).json({ ok: false }); +} From 62b915fbc18cb5af3dbc012944d5c046daf53398 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Sun, 18 Feb 2024 18:04:54 +0000 Subject: [PATCH 5/5] Added API endpoints for agents load for the homepage --- next.config.js | 15 +++++++ src/pages/api/users/agents/[code].ts | 55 +++++++++++++++++++++++ src/pages/api/users/agents/index.ts | 65 ++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 src/pages/api/users/agents/[code].ts create mode 100644 src/pages/api/users/agents/index.ts diff --git a/next.config.js b/next.config.js index 771f71d2..cdba31d8 100644 --- a/next.config.js +++ b/next.config.js @@ -35,6 +35,21 @@ const nextConfig = { }, ], }, + { + source: "/api/users/agents", + headers: [ + {key: "Access-Control-Allow-Credentials", value: "false"}, + {key: "Access-Control-Allow-Origin", value: websiteUrl}, + { + key: "Access-Control-Allow-Methods", + value: "POST,OPTIONS", + }, + { + key: "Access-Control-Allow-Headers", + value: "Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date", + }, + ], + }, ]; }, }; diff --git a/src/pages/api/users/agents/[code].ts b/src/pages/api/users/agents/[code].ts new file mode 100644 index 00000000..56e9a0fd --- /dev/null +++ b/src/pages/api/users/agents/[code].ts @@ -0,0 +1,55 @@ +import { app, adminApp } from "@/firebase"; +import { AgentUser } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { + collection, + getDocs, + getFirestore, + query, + where, +} from "firebase/firestore"; +import { getAuth } from "firebase-admin/auth"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { NextApiRequest, NextApiResponse } from "next"; +import countryCodes from "country-codes-list"; +const db = getFirestore(app); +const auth = getAuth(adminApp); + +export default withIronSessionApiRoute(user, sessionOptions); + +interface Contact { + name: string; + email: string; + number: string; +} +async function get(req: NextApiRequest, res: NextApiResponse) { + const { code } = req.query as { code: string }; + + const usersQuery = query( + collection(db, "users"), + where("type", "==", "agent"), + where("demographicInformation.country", "==", code) + ); + const docsUser = await getDocs(usersQuery); + + const docs = docsUser.docs.map((doc) => doc.data() as AgentUser); + + const entries = docs.map((user: AgentUser) => { + const newUser = { + name: user.agentInformation.companyName, + email: user.email, + number: user.demographicInformation?.phone as string, + } as Contact; + return newUser; + }) as Contact[]; + + const country = countryCodes.findOne("countryCode" as any, code); + res.json({ + label: country.countryNameEn, + entries, + }); +} + +async function user(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); +} diff --git a/src/pages/api/users/agents/index.ts b/src/pages/api/users/agents/index.ts new file mode 100644 index 00000000..8eb3da81 --- /dev/null +++ b/src/pages/api/users/agents/index.ts @@ -0,0 +1,65 @@ +import { app, adminApp } from "@/firebase"; +import { AgentUser } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { + collection, + getDocs, + getFirestore, + query, + where, +} from "firebase/firestore"; +import { getAuth } from "firebase-admin/auth"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { NextApiRequest, NextApiResponse } from "next"; +import countryCodes from "country-codes-list"; +const db = getFirestore(app); +const auth = getAuth(adminApp); + +export default withIronSessionApiRoute(user, sessionOptions); + +interface Contact { + name: string; + email: string; + number: string; +} +async function get(req: NextApiRequest, res: NextApiResponse) { + const usersQuery = query( + collection(db, "users"), + where("type", "==", "agent") + ); + const docsUser = await getDocs(usersQuery); + + const docs = docsUser.docs.map((doc) => doc.data() as AgentUser); + + const data = docs.reduce( + (acc: Record, user: AgentUser) => { + const countryCode = user.demographicInformation?.country as string; + const currentValues = acc[countryCode] || ([] as Contact[]); + const newUser = { + name: user.agentInformation.companyName, + email: user.email, + number: user.demographicInformation?.phone as string, + } as Contact; + return { + ...acc, + [countryCode]: [...currentValues, newUser], + }; + }, + {} + ) as Record; + + const result = Object.keys(data).map((code) => { + const country = countryCodes.findOne("countryCode" as any, code); + return { + label: country.countryNameEn, + key: code, + entries: data[code], + }; + }); + + res.json(result); +} + +async function user(req: NextApiRequest, res: NextApiResponse) { + if (req.method === "GET") return get(req, res); +}