diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index 358f3721..ad0799e7 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -34,8 +34,12 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro const [assignees, setAssignees] = useState(assignment?.assignees || []); const [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize})); const [isLoading, setIsLoading] = useState(false); - const [startDate, setStartDate] = useState(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "day").toDate()); - const [endDate, setEndDate] = useState(assignment ? moment(assignment.endDate).toDate() : moment().add(8, "day").toDate()); + const [startDate, setStartDate] = useState( + assignment ? moment(assignment.startDate).toDate() : moment().hours(0).minutes(0).add(1, "day").toDate(), + ); + const [endDate, setEndDate] = useState( + assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(), + ); // creates a new exam for each assignee or just one exam for all assignees const [generateMultiple, setGenerateMultiple] = useState(false); @@ -49,33 +53,26 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro }; const createAssignment = () => { - setIsLoading(true); + setIsLoading(true); - (assignment ? axios.patch : axios.post)( - `/api/assignments${assignment ? `/${assignment.id}` : ""}`, - { - assignees, - name, - startDate, - endDate, - selectedModules, - generateMultiple, - } - ) - .then(() => { - toast.success( - `The assignment "${name}" has been ${ - assignment ? "updated" : "created" - } successfully!` - ); - cancelCreation(); - }) - .catch((e) => { - console.log(e); - toast.error("Something went wrong, please try again later!"); - }) - .finally(() => setIsLoading(false)); - }; + (assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, { + assignees, + name, + startDate, + endDate, + selectedModules, + generateMultiple, + }) + .then(() => { + toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`); + cancelCreation(); + }) + .catch((e) => { + console.log(e); + toast.error("Something went wrong, please try again later!"); + }) + .finally(() => setIsLoading(false)); + }; const deleteAssignment = () => { if (assignment) { @@ -285,7 +282,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
- setGenerateMultiple(d => !d)}> + setGenerateMultiple((d) => !d)}> Generate different exams
diff --git a/src/email/index.ts b/src/email/index.ts index 0fd6bff6..c8e98add 100644 --- a/src/email/index.ts +++ b/src/email/index.ts @@ -40,3 +40,15 @@ export function prepareMailOptions(context: object, to: string[], subject: strin context, }; } + +export async function sendEmail(template: string, context: object, to: string[], subject: string): Promise { + try { + const transport = prepareMailer(template); + const mailOptions = prepareMailOptions(context, to, subject, template); + + await transport.sendMail(mailOptions); + return true; + } catch { + return false; + } +} diff --git a/src/email/templates/assignment.handlebars b/src/email/templates/assignment.handlebars new file mode 100644 index 00000000..d4a0752e --- /dev/null +++ b/src/email/templates/assignment.handlebars @@ -0,0 +1,28 @@ + + + + + + + + +
+

Hello {{user.name}},

+
+

You have just been given the assignment "{{assignment.name}}" by your teacher {{assignment.assigner}}!

+
+

It's start date will be on {{assignment.startDate}} and will only last until {{assignment.endDate}} +

+
+

For this assignment, you've been tasked with completing exams of the following modules: + {{assignment.modules}}. +

+
+

Don't forget to do it before its end date!

+

Click here to open the EnCoach Platform!

+
+

Thanks,

+

Your EnCoach team

+
+ + \ No newline at end of file diff --git a/src/email/templates/assignment.handlebars.json b/src/email/templates/assignment.handlebars.json new file mode 100644 index 00000000..08486418 --- /dev/null +++ b/src/email/templates/assignment.handlebars.json @@ -0,0 +1,13 @@ +{ + "user": { + "name": "Tiago Ribeiro" + }, + "assignment": { + "name": "Final Exam", + "assigner": "Teacher", + "assignees": [], + "modules": "Reading and Writing", + "startDate": "24/12/2023", + "endDate": "27/01/2024" + } +} \ No newline at end of file diff --git a/src/pages/api/assignments/index.ts b/src/pages/api/assignments/index.ts index f5a87acd..a34228d1 100644 --- a/src/pages/api/assignments/index.ts +++ b/src/pages/api/assignments/index.ts @@ -1,14 +1,17 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; import {app} from "@/firebase"; -import {getFirestore, collection, getDocs, query, where, setDoc, doc} from "firebase/firestore"; +import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {uuidv4} from "@firebase/util"; -import { Module } from "@/interfaces"; -import { getExams } from "@/utils/exams.be"; -import { Exam } from "@/interfaces/exam"; -import { flatten } from "lodash"; +import {Module} from "@/interfaces"; +import {getExams} from "@/utils/exams.be"; +import {Exam} from "@/interfaces/exam"; +import {capitalize, flatten} from "lodash"; +import {User} from "@/interfaces/user"; +import moment from "moment"; +import {sendEmail} from "@/email"; const db = getFirestore(app); @@ -39,106 +42,108 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { } interface ExamWithUser { - module: Module; - id: string; - assignee: string; + module: Module; + id: string; + assignee: string; } function getRandomIndex(arr: any[]): number { - const randomIndex = Math.floor(Math.random() * arr.length); - return randomIndex; + const randomIndex = Math.floor(Math.random() * arr.length); + return randomIndex; } -const generateExams = async ( - generateMultiple: Boolean, - selectedModules: Module[], - assignees: string[] -): Promise => { - if (generateMultiple) { - // for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once - const allExams = await assignees.map(async (assignee) => { - const selectedModulePromises = await selectedModules.map( - async (module: Module) => { - try { - const exams: Exam[] = await getExams(db, module, "true", assignee); +const generateExams = async (generateMultiple: Boolean, selectedModules: Module[], assignees: string[]): Promise => { + if (generateMultiple) { + // for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once + const allExams = await assignees.map(async (assignee) => { + const selectedModulePromises = await selectedModules.map(async (module: Module) => { + try { + const exams: Exam[] = await getExams(db, module, "true", assignee); - const exam = exams[getRandomIndex(exams)]; - if (exam) { - return { module: exam.module, id: exam.id, assignee }; - } - return null; - } catch (e) { - console.error(e); - return null; - } - }, - [] - ); - const newModules = await Promise.all(selectedModulePromises); + const exam = exams[getRandomIndex(exams)]; + if (exam) { + return {module: exam.module, id: exam.id, assignee}; + } + return null; + } catch (e) { + console.error(e); + return null; + } + }, []); + const newModules = await Promise.all(selectedModulePromises); - return newModules; - }, []); + return newModules; + }, []); - const exams = flatten(await Promise.all(allExams)).filter( - (x) => x !== null - ) as ExamWithUser[]; - return exams; - } + const exams = flatten(await Promise.all(allExams)).filter((x) => x !== null) as ExamWithUser[]; + return exams; + } - const selectedModulePromises = await selectedModules.map( - async (module: Module) => { - const exams: Exam[] = await getExams(db, module, "false", undefined); - const exam = exams[getRandomIndex(exams)]; + const selectedModulePromises = await selectedModules.map(async (module: Module) => { + const exams: Exam[] = await getExams(db, module, "false", undefined); + const exam = exams[getRandomIndex(exams)]; - if (exam) { - return { module: exam.module, id: exam.id }; - } - return null; - } - ); + if (exam) { + return {module: exam.module, id: exam.id}; + } + return null; + }); - const exams = await Promise.all(selectedModulePromises); - const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[]; - return flatten( - assignees.map((assignee) => - examesFiltered.map((exam) => ({ ...exam, assignee })) - ) - ); + const exams = await Promise.all(selectedModulePromises); + const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[]; + return flatten(assignees.map((assignee) => examesFiltered.map((exam) => ({...exam, assignee})))); }; async function POST(req: NextApiRequest, res: NextApiResponse) { - const { - selectedModules, - assignees, - // Generarte multiple true would generate an unique exam for eacah user - // false would generate the same exam for all usersa - generateMultiple = false, - ...body - } = req.body as { - selectedModules: Module[]; - assignees: string[]; - generateMultiple: Boolean; - }; + const { + selectedModules, + assignees, + // Generate multiple true would generate an unique exam for each user + // false would generate the same exam for all users + generateMultiple = false, + ...body + } = req.body as { + selectedModules: Module[]; + assignees: string[]; + generateMultiple: Boolean; + name: string; + startDate: string; + endDate: string; + }; - const exams: ExamWithUser[] = await generateExams( - generateMultiple, - selectedModules, - assignees - ); - if (exams.length === 0) { - res - .status(400) - .json({ ok: false, error: "No exams found for the selected modules" }); - return; - } + const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees); - await setDoc(doc(db, "assignments", uuidv4()), { - assigner: req.session.user?.id, - assignees, - results: [], - exams, - ...body, - }); + if (exams.length === 0) { + res.status(400).json({ok: false, error: "No exams found for the selected modules"}); + return; + } - res.status(200).json({ ok: true }); + await setDoc(doc(db, "assignments", uuidv4()), { + assigner: req.session.user?.id, + assignees, + results: [], + exams, + ...body, + }); + + res.status(200).json({ok: true}); + + for (const assigneeID of assignees) { + const assigneeSnapshot = await getDoc(doc(db, "users", assigneeID)); + if (!assigneeSnapshot.exists()) continue; + + const assignee = {id: assigneeID, ...assigneeSnapshot.data()} as User; + const name = body.name; + const teacher = req.session.user!; + const examModulesLabel = exams.map((x) => capitalize(x.module)).join(", "); + const startDate = moment(body.startDate).format("DD/MM/YYYY - HH:mm"); + const endDate = moment(body.endDate).format("DD/MM/YYYY - HH:mm"); + + await sendEmail( + "assignment", + {user: {name: assignee.name}, assignment: {name, startDate, endDate, modules: examModulesLabel, assigner: teacher.name}}, + [assignee.email], + "EnCoach - New Assignment!", + ); + } } diff --git a/src/pages/api/reset/sendVerification.ts b/src/pages/api/reset/sendVerification.ts index cf028f59..cb582cb1 100644 --- a/src/pages/api/reset/sendVerification.ts +++ b/src/pages/api/reset/sendVerification.ts @@ -4,7 +4,7 @@ import {getAuth as getAdminAuth, UserRecord} from "firebase-admin/auth"; import {app, adminApp} from "@/firebase"; import {sessionOptions} from "@/lib/session"; import {withIronSessionApiRoute} from "iron-session/next"; -import {prepareMailer, prepareMailOptions} from "@/email"; +import {prepareMailer, prepareMailOptions, sendEmail} from "@/email"; import ShortUniqueId from "short-unique-id"; export default withIronSessionApiRoute(sendVerification, sessionOptions); @@ -13,8 +13,8 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) { const short = new ShortUniqueId(); if (req.session.user) { - const transport = prepareMailer("verification"); - const mailOptions = prepareMailOptions( + const ok = await sendEmail( + "verification", { name: req.session.user.name, code: short.randomUUID(6), @@ -22,12 +22,9 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) { }, [req.session.user.email], "EnCoach Verification", - "verification", ); - await transport.sendMail(mailOptions); - - res.status(200).json({ok: true}); + res.status(200).json({ok}); return; } res.status(404).json({ok: false});