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
106 changes: 92 additions & 14 deletions backend/controllers/courseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,33 @@ import Course from "../models/courseModel";
import Rating from "../models/ratingModel";
import User, { IUser } from "../models/userModel";
import Progress, { IProgress } from "../models/progressModel";
import Survey from "../models/surveyModel";
import Question from "../models/questionModel";
import { emailQueue } from "../jobs/emailQueue";
import { AuthenticatedRequest } from "../middlewares/authMiddleware";

// Returns { hasPre, hasPost } for the survey attached to a course (if any).
async function getSurveyPhasePresence(
courseId: string | { toString(): string }
): Promise<{ hasPre: boolean; hasPost: boolean }> {
const course = await Course.findById(courseId).select("surveyId");
if (!course || !course.surveyId) return { hasPre: false, hasPost: false };
const survey = await Survey.findById(course.surveyId).select("questions");
if (!survey || !survey.questions || survey.questions.length === 0) {
return { hasPre: false, hasPost: false };
}
const questions = await Question.find({
_id: { $in: survey.questions },
}).select("phase");
let hasPre = false;
let hasPost = false;
for (const q of questions) {
if (q.phase === "pre") hasPre = true;
else hasPost = true;
}
return { hasPre, hasPost };
}

// Define an interface for error objects
interface ErrorWithDetails {
message?: string;
Expand Down Expand Up @@ -554,7 +578,8 @@ export const dropCourseEnrollment = async (
isComplete: false,
completedComponents: {
webinar: false,
survey: false,
preSurvey: false,
postSurvey: false,
certificate: false,
},
dateCompleted: null,
Expand Down Expand Up @@ -645,7 +670,8 @@ export const getCourseProgress = async (
isComplete: false,
completedComponents: {
webinar: false,
survey: false,
preSurvey: false,
postSurvey: false,
certificate: false,
},
dateCompleted: null,
Expand Down Expand Up @@ -730,7 +756,8 @@ export const getUserCourseProgress = async (
isComplete: false,
completedComponents: {
webinar: false,
survey: false,
preSurvey: false,
postSurvey: false,
certificate: false,
},
dateCompleted: null,
Expand Down Expand Up @@ -763,13 +790,19 @@ export const updateUserProgress = async (
): Promise<void> => {
try {
const { courseId, userId } = req.params;
const { webinarComplete, surveyComplete, certificateComplete } = req.body;
const {
webinarComplete,
preSurveyComplete,
postSurveyComplete,
certificateComplete,
} = req.body;

console.log("=== Progress Update Request ===");
console.log("Params:", { courseId, userId });
console.log("Body:", {
webinarComplete,
surveyComplete,
preSurveyComplete,
postSurveyComplete,
certificateComplete,
});

Expand All @@ -790,11 +823,31 @@ export const updateUserProgress = async (

console.log("Found progress:", progress._id);

// Update completed components
// Merge with existing values so a single-phase update doesn't clobber the other.
const existing = progress.completedComponents || {};
const legacySurvey = Boolean(existing.survey);

const { hasPre, hasPost } = await getSurveyPhasePresence(courseId);

const completedComponents = {
webinar: Boolean(webinarComplete),
survey: Boolean(surveyComplete),
certificate: Boolean(certificateComplete),
webinar:
webinarComplete !== undefined
? Boolean(webinarComplete)
: Boolean(existing.webinar),
preSurvey: hasPre
? preSurveyComplete !== undefined
? Boolean(preSurveyComplete)
: Boolean(existing.preSurvey ?? legacySurvey)
: true,
postSurvey: hasPost
? postSurveyComplete !== undefined
? Boolean(postSurveyComplete)
: Boolean(existing.postSurvey ?? legacySurvey)
: true,
certificate:
certificateComplete !== undefined
? Boolean(certificateComplete)
: Boolean(existing.certificate),
};

console.log("Setting completed components:", completedComponents);
Expand Down Expand Up @@ -855,9 +908,16 @@ export const batchUpdateUserProgress = async (

const results: any[] = [];

const { hasPre, hasPost } = await getSurveyPhasePresence(courseId);

for (const update of updates) {
const { userId, webinarComplete, surveyComplete, certificateComplete } =
update;
const {
userId,
webinarComplete,
preSurveyComplete,
postSurveyComplete,
certificateComplete,
} = update;

console.log("Processing update for user:", userId);

Expand All @@ -876,10 +936,28 @@ export const batchUpdateUserProgress = async (
continue;
}

const existing = progress.completedComponents || {};
const legacySurvey = Boolean(existing.survey);

const completedComponents = {
webinar: Boolean(webinarComplete),
survey: Boolean(surveyComplete),
certificate: Boolean(certificateComplete),
webinar:
webinarComplete !== undefined
? Boolean(webinarComplete)
: Boolean(existing.webinar),
preSurvey: hasPre
? preSurveyComplete !== undefined
? Boolean(preSurveyComplete)
: Boolean(existing.preSurvey ?? legacySurvey)
: true,
postSurvey: hasPost
? postSurveyComplete !== undefined
? Boolean(postSurveyComplete)
: Boolean(existing.postSurvey ?? legacySurvey)
: true,
certificate:
certificateComplete !== undefined
? Boolean(certificateComplete)
: Boolean(existing.certificate),
};

progress.completedComponents = completedComponents;
Expand Down
8 changes: 6 additions & 2 deletions backend/controllers/surveyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ export const updateSurvey = async(req: Request, res: Response) : Promise<void> =
// Check if there are existing responses for this survey
const responseCount = await SurveyResponse.countDocuments({surveyId: survey._id});

if (responseCount === 0) {
// If no responses AND (no partial course selection OR all courses selected), safe to update in place
const allCourseIds = survey.courseIds.map((id: any) => id.toString());
const isPartialUpdate = courseIdsToUpdate && courseIdsToUpdate.length < allCourseIds.length;

if (responseCount === 0 && !isPartialUpdate) {
// No responses, safe to update in place
if (name) survey.name = name;
if (questions) survey.questions = questions;
Expand All @@ -120,7 +124,7 @@ export const updateSurvey = async(req: Request, res: Response) : Promise<void> =
return;
}

const coursesToUpdate = courseIdsToUpdate || survey.courseIds.map(id => id.toString());
const coursesToUpdate = courseIdsToUpdate || allCourseIds;

const coursesStaying = survey.courseIds.map(id => id.toString()).filter(id => !coursesToUpdate.includes(id));

Expand Down
69 changes: 48 additions & 21 deletions backend/controllers/surveyResponseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ export const getSurveyResponses = async (
res: Response
): Promise<void> => {
try {
const { userId, surveyId, courseId, startDate, endDate } = req.query;
const { userId, surveyId, courseId, startDate, endDate, phase } = req.query;

let filter: any = {};

if (userId) filter.userId = userId;
if (surveyId) filter.surveyId = surveyId;
if (courseId) filter.courseId = courseId;
if (phase === "pre" || phase === "post") filter.phase = phase;

if (startDate || endDate) {
filter.dateCompleted = {};
Expand Down Expand Up @@ -62,7 +63,7 @@ export const createSurveyResponse = async (
): Promise<void> => {
try {
// answers are a list of QuestionResponse IDs and QuestionResponse objects must be created first before calling this endpoint
const { userId, answers, surveyId, courseId } = req.body;
const { userId, answers, surveyId, courseId, phase } = req.body;

if (!userId || !answers || answers.length === 0) {
res.status(400).json({
Expand All @@ -80,12 +81,20 @@ export const createSurveyResponse = async (
return;
}

if (phase !== "pre" && phase !== "post") {
res.status(400).json({
success: false,
message: "Please provide a valid phase ('pre' or 'post').",
});
return;
}

const newSurveyResponse = new SurveyResponse({
userId,
answers,
surveyId,
courseId,
phase,
});

const savedSurveyResponse = await newSurveyResponse.save();
Expand Down Expand Up @@ -152,18 +161,23 @@ export const deleteSurveyResponse = async (req: Request, res: Response): Promise
// }
export const getSurveyResponseStats = async (req: Request, res: Response): Promise<void> => {
try {
const { surveyId, courseId } = req.query;
const { surveyId, courseId, phase } = req.query;
const matchStage: any = {};

if (surveyId) matchStage.surveyId = new mongoose.Types.ObjectId(surveyId as string);
if (courseId) matchStage.courseId = new mongoose.Types.ObjectId(courseId as string);
if (phase === "pre" || phase === "post") matchStage.phase = phase;

// Group responses by survey + courses
// Group responses by survey + course + phase
const grouped = await SurveyResponse.aggregate([
{ $match: matchStage },
{
$group: {
_id: { surveyId: "$surveyId", courseId: "$courseId" },
_id: {
surveyId: "$surveyId",
courseId: "$courseId",
phase: { $ifNull: ["$phase", "post"] },
},
totalResponses: { $sum: 1 },
},
},
Expand All @@ -177,15 +191,24 @@ export const getSurveyResponseStats = async (req: Request, res: Response): Promi

if (!survey) continue;

// Get all QuestionResponse IDs for this survey+course combo
const answerIds = await SurveyResponse.find({
// Get all QuestionResponse IDs for this survey+course+phase combo
const phaseFilter: any = {
surveyId: group._id.surveyId,
courseId: group._id.courseId,
}).distinct("answers");
};
if (group._id.phase === "pre") {
phaseFilter.phase = "pre";
} else {
phaseFilter.$or = [{ phase: "post" }, { phase: { $exists: false } }];
}
const answerIds = await SurveyResponse.find(phaseFilter).distinct("answers");

// For each question in the survey, calculate breakdown of answers
// For each question in the survey matching this phase, calculate breakdown
const phaseQuestions = (survey.questions as any[]).filter(
(q) => (q.phase || "post") === group._id.phase
);
const questionStats = [];
for (const question of survey.questions as any[]) {
for (const question of phaseQuestions) {
const questionResponses = await QuestionResponse.find({
questionId: question._id,
_id: { $in: answerIds },
Expand Down Expand Up @@ -217,6 +240,7 @@ export const getSurveyResponseStats = async (req: Request, res: Response): Promi
surveyVersion: survey.version,
courseId: group._id.courseId,
courseName: course?.className || "Unknown Course",
phase: group._id.phase,
totalResponses: group.totalResponses,
questions: questionStats,
});
Expand All @@ -233,11 +257,12 @@ export const getSurveyResponseStats = async (req: Request, res: Response): Promi
// @route GET /api/surveyResponses/export?surveyId=&courseId=&format=row-per-response|row-per-answer
export const exportSurveyResponses = async (req: Request, res: Response): Promise<void> => {
try {
const { surveyId, courseId, format = "row-per-response" } = req.query;
const { surveyId, courseId, format = "row-per-response", phase } = req.query;
const filter: any = {};

if (surveyId) filter.surveyId = surveyId;
if (courseId) filter.courseId = courseId;
if (phase === "pre" || phase === "post") filter.phase = phase;

const responses = await SurveyResponse.find(filter)
.populate("answers")
Expand All @@ -256,19 +281,20 @@ export const exportSurveyResponses = async (req: Request, res: Response): Promis
if (format === "row-per-answer") {
// Normalized: one row per question-answer pair
const headers = [
"SurveyName", "CourseName", "UserId", "SubmittedAt",
"SurveyName", "CourseName", "Phase", "UserId", "SubmittedAt",
"QuestionId", "QuestionText", "AnswerType", "Answer",
];

const rows: string[][] = [];
for (const resp of responses as any[]) {
const surveyName = resp.surveyId?.name || "Unknown";
const courseName = resp.courseId?.className || "Unknown";

const respPhase = resp.phase || "post";

for (const answer of resp.answers) {
const question = await Question.findById(answer.questionId);
rows.push([
surveyName, courseName, resp.userId,
surveyName, courseName, respPhase, resp.userId,
resp.createdAt?.toISOString() || "",
answer.questionId?.toString() || "",
question?.question || "",
Expand Down Expand Up @@ -297,22 +323,23 @@ export const exportSurveyResponses = async (req: Request, res: Response): Promis
}

const headers = [
"SurveyName", "CourseName", "UserId", "SubmittedAt",
"SurveyName", "CourseName", "Phase", "UserId", "SubmittedAt",
...allQuestions.map((q, i) => `${i + 1}. ${q.question}`),
];

const rows: string[][] = [];
for (const resp of responses as any[]) {
const surveyName = resp.surveyId?.name || "Unknown";
const courseName = resp.courseId?.className || "Unknown";

const respPhase = resp.phase || "post";

const answerMap = new Map<string, string>();
for (const answer of resp.answers) {
answerMap.set(answer.questionId?.toString() || "", answer.answer || "");
}

rows.push([
surveyName, courseName, resp.userId,
surveyName, courseName, respPhase, resp.userId,
resp.createdAt?.toISOString() || "",
...allQuestions.map((q) => answerMap.get(q._id.toString()) || ""),
]);
Expand Down
7 changes: 7 additions & 0 deletions backend/models/questionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface IQuestion extends Document {
answerType: string;
answers?: string[];
isRequired: boolean;
phase: "pre" | "post";
}

// Define the schema with placeholders for fields (others will fill this in)
Expand All @@ -17,6 +18,12 @@ const QuestionSchema: Schema = new Schema(
answerType: { type: String, required: true },
answers: [{ type: String }],
isRequired: { type: Boolean, required: true },
phase: {
type: String,
enum: ["pre", "post"],
default: "post",
required: true,
},
},
{
timestamps: true,
Expand Down
Loading
Loading