diff --git a/apps/hub-server/.env.example b/apps/hub-server/.env.example index 67a41c4e..7780c708 100644 --- a/apps/hub-server/.env.example +++ b/apps/hub-server/.env.example @@ -1,2 +1,6 @@ MOODLE_TOKEN= -MOODLE_URL= \ No newline at end of file +MOODLE_URL= +MAIL_SERVER= +MAIL_USER= +MAIL_PASSWORD= +MAIL_PORT= \ No newline at end of file diff --git a/apps/hub-server/modules/chron.ts b/apps/hub-server/modules/chron.ts index 52350462..e07208c4 100644 --- a/apps/hub-server/modules/chron.ts +++ b/apps/hub-server/modules/chron.ts @@ -2,6 +2,8 @@ import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle"; import { CronJob } from "cron"; import { prisma } from "@repo/db"; import { eventCompleted } from "@repo/ui/helper"; +import { sendCourseCompletedEmail } from "modules/mail"; +import { handleParticipantFinished } from "modules/event"; const syncMoodleIds = async () => { try { @@ -57,7 +59,7 @@ const updateParticipantMoodleResults = async () => { ); if (quizzResult?.completionstatus?.completed === true) { - await prisma.participant.update({ + return prisma.participant.update({ where: { id: p.id, }, @@ -65,7 +67,7 @@ const updateParticipantMoodleResults = async () => { finisherMoodleCurseCompleted: true, statusLog: { push: { - event: "Finisher course completed", + event: "Moodle-Kurs abgeschlossen", timestamp: new Date(), user: "system", }, @@ -77,33 +79,40 @@ const updateParticipantMoodleResults = async () => { ); }; -export const checkedFinishedParticipants = async () => { +export const checkFinishedParticipants = async () => { + console.log("Checking finished participants"); const participantsPending = await prisma.participant.findMany({ where: { - finished: false, + completetionWorkflowFinished: false, }, include: { Event: true, User: true, }, }); - participantsPending.forEach(async (p) => { if (!p.User) return; - if (!p.User.moodleId) return; - const completed = eventCompleted(p.Event, p); + + if (!completed) return; + handleParticipantFinished(p.Event, p, p.User); }); }; CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true }); CronJob.from({ - cronTime: "*/5 * * * *", + cronTime: "*/1 * * * *", onTick: async () => { console.log("Updating participant moodle results"); await updateParticipantMoodleResults(); + await checkFinishedParticipants(); }, start: true, }); -updateParticipantMoodleResults(); +const debug = async () => { + await updateParticipantMoodleResults(); + await checkFinishedParticipants(); +}; + +debug(); diff --git a/apps/hub-server/modules/event.ts b/apps/hub-server/modules/event.ts index 53c6a16b..94b100fe 100644 --- a/apps/hub-server/modules/event.ts +++ b/apps/hub-server/modules/event.ts @@ -1,4 +1,5 @@ import { Event, Participant, prisma, User } from "@repo/db"; +import { sendCourseCompletedEmail } from "modules/mail"; export const handleParticipantFinished = async ( event: Event, @@ -11,9 +12,6 @@ export const handleParticipantFinished = async ( }, }); - //TODO: Send Discord Message - //TODO: Send Email - await prisma.user.update({ where: { id: user.id, @@ -22,19 +20,24 @@ export const handleParticipantFinished = async ( badges: { push: event.finishedBadges, }, - permissions: event.finishedPermissions, + permissions: { + push: event.finishedPermissions, + }, }, }); + //TODO: Send Discord Message + await sendCourseCompletedEmail(user.email, user, event); + await prisma.participant.update({ where: { id: participant.id, }, data: { - finished: true, + completetionWorkflowFinished: true, statusLog: { push: { - event: "Event finished", + event: "Berechtigungen und Badges vergeben", timestamp: new Date(), user: "system", }, diff --git a/apps/hub-server/modules/mail-templates/CourseCompleted.tsx b/apps/hub-server/modules/mail-templates/CourseCompleted.tsx new file mode 100644 index 00000000..0e4bd22f --- /dev/null +++ b/apps/hub-server/modules/mail-templates/CourseCompleted.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { Event, User } from "@repo/db"; + +import { Html, Button, render } from "@react-email/components"; + +const Template = ({ event, user }: { user: User; event: Event }) => ( + +
You completed the Course {event.name}
+Congratulation
+ +); + +export function renderCourseCompleted({ + user, + event, +}: { + user: User; + event: Event; +}) { + return render(); +} diff --git a/apps/hub-server/modules/mail.ts b/apps/hub-server/modules/mail.ts new file mode 100644 index 00000000..af242778 --- /dev/null +++ b/apps/hub-server/modules/mail.ts @@ -0,0 +1,59 @@ +import { Event, User } from "@repo/db"; +import nodemailer from "nodemailer"; +import { renderCourseCompleted } from "./mail-templates/CourseCompleted"; + +let transporter: nodemailer.Transporter | null = null; + +const initTransporter = () => { + if (!process.env.MAIL_SERVER) + return console.error("MAIL_SERVER is not defined"); + if (!process.env.MAIL_PORT) return console.error("MAIL_PORT is not defined"); + if (!process.env.MAIL_USER) return console.error("MAIL_USER is not defined"); + if (!process.env.MAIL_PASSWORD) + return console.error("MAIL_PASSWORD is not defined"); + + transporter = nodemailer.createTransport({ + host: process.env.MAIL_SERVER, + port: parseInt(process.env.MAIL_PORT), + secure: true, // true for 465, false for other ports + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASSWORD, + }, + }); + transporter.on("error", (err) => { + console.error("Mail occurred:", err); + }); + transporter.on("idle", () => { + console.log("Mail Idle"); + }); +}; + +initTransporter(); + +export const sendCourseCompletedEmail = async ( + to: string, + user: User, + event: Event, +) => { + const emailHtml = await renderCourseCompleted({ user, event }); + + if (!transporter) { + console.error("Transporter is not initialized"); + return; + } + transporter.sendMail( + { + from: process.env.MAIL_USER, + to, + subject: `Congratulations ${user.firstname} on completing ${event.name}`, + html: emailHtml, + }, + (error, info) => { + if (error) { + console.error("Error sending email:", error); + } else { + } + }, + ); +}; diff --git a/apps/hub-server/package.json b/apps/hub-server/package.json index f3994044..0699910a 100644 --- a/apps/hub-server/package.json +++ b/apps/hub-server/package.json @@ -11,12 +11,16 @@ "@repo/db": "*", "@repo/typescript-config": "*", "@types/node": "^22.13.5", + "@types/nodemailer": "^6.4.17", "concurrently": "^9.1.2", "typescript": "latest" }, "dependencies": { + "@react-email/components": "^0.0.33", "axios": "^1.7.9", "cron": "^4.1.0", - "dotenv": "^16.4.7" + "dotenv": "^16.4.7", + "nodemailer": "^6.10.0", + "react": "^19.0.0" } } diff --git a/apps/hub-server/tsconfig.json b/apps/hub-server/tsconfig.json index 881d6075..ad90dfe0 100644 --- a/apps/hub-server/tsconfig.json +++ b/apps/hub-server/tsconfig.json @@ -1,10 +1,11 @@ { - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "outDir": "dist", - "allowImportingTsExtensions": false, - "baseUrl": "." - }, - "include": ["**/*.ts", "./index.ts"], - "exclude": ["node_modules", "dist"] - } \ No newline at end of file + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "allowImportingTsExtensions": false, + "baseUrl": ".", + "jsx": "react" + }, + "include": ["**/*.ts", "./index.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx index b4f36d59..eb45abdc 100644 --- a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx +++ b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx @@ -22,33 +22,37 @@ import { Switch } from "../../../../_components/ui/Switch"; interface AppointmentModalProps { event?: Event; ref: RefObject