Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions backend/controllers/courseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Course from "../models/courseModel";
import Rating from "../models/ratingModel";
import User, { IUser } from "../models/userModel";
import Progress, { IProgress } from "../models/progressModel";
import { emailQueue } from "../jobs/emailQueue";

// Define an interface for error objects
interface ErrorWithDetails {
Expand Down Expand Up @@ -446,6 +447,110 @@ export const getCourseUsers = async (
}
};

// @desc Drop a user from a course and auto-enroll from waitlist
// @route POST /api/courses/:courseId/drop
// @access Public
export const dropCourseEnrollment = async (
req: Request,
res: Response
): Promise<void> => {
try {
const { courseId } = req.params;
const { userId } = req.body;

if (!courseId || !userId) {
res.status(400).json({
success: false,
message: "Course ID and User ID are required.",
});
return;
}

const course = await Course.findById(courseId);
if (!course) {
res.status(404).json({
success: false,
message: "Course not found.",
});
return;
}

const beforeCount = course.students.length;
course.students = course.students.filter(
(id) => id.toString() !== userId.toString()
);

if (beforeCount === course.students.length) {
res.status(404).json({
success: false,
message: "User not enrolled in this course.",
});
return;
}

await Progress.deleteMany({ course: courseId, user: userId });

let promotedUserId: string | null = null;

const limit = course.registrationLimit || 0;
const hasCapacity = limit === 0 || course.students.length < limit;

if (hasCapacity && course.waitlist && course.waitlist.length > 0) {
course.waitlist.sort(
(a, b) =>
new Date(a.joinedAt).getTime() - new Date(b.joinedAt).getTime()
);
const nextInLine = course.waitlist.shift();
if (nextInLine) {
const promotedId = nextInLine.user.toString();
course.students.push(nextInLine.user as any);
promotedUserId = promotedId;

const existingProgress = await Progress.findOne({
course: courseId,
user: promotedId,
});
if (!existingProgress) {
await new Progress({
course: courseId,
user: promotedId,
isComplete: false,
completedComponents: {
webinar: false,
survey: false,
certificate: false,
},
dateCompleted: null,
}).save();
}

await emailQueue.add("registration-confirmation", {
userId: promotedId,
courseId,
});
await emailQueue.add("waitlist-promotion", {
userId: promotedId,
courseId,
});
}
}

await course.save();

res.status(200).json({
success: true,
message: "User dropped from course.",
promotedUserId,
});
} catch (error: any) {
console.error("Error dropping course enrollment:", error);
res.status(500).json({
success: false,
message: error.message || "Internal server error.",
});
}
};

// @desc Get progress for all users in a course
// @route GET /api/courses/:courseId/progress
// @access Public
Expand Down
61 changes: 53 additions & 8 deletions backend/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Payment from "../models/paymentModel";
import Course from "../models/courseModel";
import mongoose from "mongoose";
import { AuthenticatedRequest } from "../middlewares/authMiddleware";
import { emailQueue } from "../jobs/emailQueue";

export const getUsers = async (req: Request, res: Response): Promise<void> => {
try {
Expand Down Expand Up @@ -232,8 +233,45 @@ export const register = async (req: Request, res: Response): Promise<void> => {
throw new Error(`Course with ID ${courseId} not found.`);
}

const studentId = new mongoose.Types.ObjectId(userId);
const alreadyEnrolled = course.students.some(
(id) => id.toString() === userId.toString()
);
if (alreadyEnrolled) {
return {
courseId,
status: "already-enrolled",
};
}

const limit = course.registrationLimit || 0;
const isFull = limit > 0 && course.students.length >= limit;
const existingWaitlistEntry = course.waitlist?.find(
(entry) => entry.user.toString() === userId.toString()
);

if (isFull) {
if (!existingWaitlistEntry) {
course.waitlist = course.waitlist || [];
course.waitlist.push({ user: studentId, joinedAt: new Date() });
course.waitlist.sort(
(a, b) =>
new Date(a.joinedAt).getTime() - new Date(b.joinedAt).getTime()
);
await course.save();
await emailQueue.add("waitlist-confirmation", {
userId: userId.toString(),
courseId: courseId.toString(),
});
}
return {
courseId,
status: "waitlisted",
};
}

const progress = new Progress({
user: new mongoose.Types.ObjectId(userId),
user: studentId,
course: new mongoose.Types.ObjectId(courseId),
isComplete: false,
completedComponents: {
Expand All @@ -245,22 +283,29 @@ export const register = async (req: Request, res: Response): Promise<void> => {
});
await progress.save();

console.log("progress", progress);

if (!course.students.includes(new mongoose.Types.ObjectId(userId))) {
course.students.push(new mongoose.Types.ObjectId(userId));
if (!course.students.some((id) => id.toString() === userId.toString())) {
course.students.push(studentId);
await course.save();
}

return progress;
await emailQueue.add("registration-confirmation", {
userId: userId.toString(),
courseId: courseId.toString(),
});

return {
courseId,
status: "enrolled",
progressId: progress._id,
};
});

const progressResults = await Promise.all(progressPromises);

res.status(201).json({
success: true,
message: "User registered to courses successfully.",
progress: progressResults,
message: "User processed for course registration.",
results: progressResults,
});
} catch (error) {
console.error(error);
Expand Down
8 changes: 8 additions & 0 deletions backend/models/courseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface ICourse extends Document {
shortUrl: string;
draft: boolean;
registrationLimit: number;
waitlist: { user: mongoose.Types.ObjectId | IUser; joinedAt: Date }[];
}

const CourseSchema: Schema = new Schema(
Expand Down Expand Up @@ -98,6 +99,13 @@ const CourseSchema: Schema = new Schema(
shortUrl: { type: String, required: false },
draft: { type: Boolean, required: true, default: true },
registrationLimit: { type: Number, required: false, default: 0 },
waitlist: [
{
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
joinedAt: { type: Date, required: true, default: Date.now },
_id: false,
},
],
},
{
timestamps: true,
Expand Down
4 changes: 4 additions & 0 deletions backend/routes/courseRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
updateUserProgress,
batchUpdateUserProgress,
getUserCourseProgress,
dropCourseEnrollment,
} from "../controllers/courseController";

const router = express.Router();
Expand All @@ -32,6 +33,9 @@ router.delete("/:id", deleteCourse);
// GET all users enrolled in a course
router.get("/:courseId/users", getCourseUsers);

// Drop a user and auto-enroll from waitlist if available
router.post("/:courseId/drop", dropCourseEnrollment);

// GET progress for all users in a course
router.get("/:courseId/progress", getCourseProgress);

Expand Down
68 changes: 67 additions & 1 deletion backend/workers/emailWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import mongoose from "mongoose";
import Email from "../models/emailModel";
import User from "../models/userModel";
import { sendEmail } from "../config/resend";
import { ICourse } from "../models/courseModel";
import Course, { ICourse } from "../models/courseModel";
import connectDB from "../config/db";
import "../models/courseModel";
import { emailQueue } from "../jobs/emailQueue";
Expand Down Expand Up @@ -54,6 +54,72 @@ const recoverMissedEmails = async () => {
try {
console.log("📦 Worker picked up job:", job.id, job.data);

if (job.name === "waitlist-confirmation") {
const { userId, courseId } = job.data;
const [user, course] = await Promise.all([
User.findById(userId),
Course.findById(courseId),
]);
if (user && course) {
await sendEmail(
user.email,
`You are waitlisted for ${course.className}`,
`<p>Hi {{name}},</p><p>You have been added to the waitlist for {{course}}. We'll email you if a seat opens.</p>`,
{ name: user.name, course: course.className }
);
}
return;
}

if (job.name === "waitlist-promotion") {
const { userId, courseId } = job.data;
const [user, course] = await Promise.all([
User.findById(userId),
Course.findById(courseId),
]);
if (user && course) {
await sendEmail(
user.email,
`You're in! ${course.className}`,
`<p>Hi {{name}},</p><p>A seat opened up and you are now enrolled in {{course}}.</p>`,
{ name: user.name, course: course.className }
);
}
return;
}

if (job.name === "registration-confirmation") {
const { userId, courseId } = job.data;
const [user, course] = await Promise.all([
User.findById(userId),
Course.findById(courseId),
]);
if (user && course) {
await sendEmail(
user.email,
`Registered for ${course.className}`,
`<p>Hi {{name}},</p><p>You are registered for {{course}}.</p>`,
{ name: user.name, course: course.className }
);
}
return;
}

if (job.name === "course-reminder") {
console.log("course-reminder ");
return;
}

if (job.name === "speaker-assignment") {
console.log("speaker-assignment");
return;
}

if (job.name !== "send-course-email") {
console.log(`No handler for job type '${job.name}', skipping.`);
return;
}

const email = await Email.findById(job.data.emailId).populate("course");
if (!email) {
console.warn("⚠️ Email not found for ID:", job.data.emailId);
Expand Down