add: ver()

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-08 11:31:40 +08:00
parent 0e4cd86119
commit bb7e8fde08
29 changed files with 2234 additions and 780 deletions
+198 -274
View File
@@ -1,21 +1,39 @@
"use strict";
/***********************************************************************************************************************************************************************
* File Name: lessons.controller.js (admin)
* Type of Program: Controller
* Description: Standalone Lesson library — Lessons live independently of Units.
*
* /admin/lessons → library CRUD (list / create / update / archive / restore / permanent delete)
* /admin/lessons/:lessonId/page → the lesson's block content (unchanged contract)
*
* Membership in a unit is a unit_lessons row (managed from the unit editor /
* course builder); archiving here removes the Lesson from every unit at once.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 7, 2026 (junction revamp — Units/Lessons run independently)
***********************************************************************************************************************************************************************/
const { Op } = require("sequelize");
const sequelize = require("../../config/db.config");
const R = require("../../utils/response.util");
const { paginate } = require("../../utils/paginate.util");
const { recomputeDurations } = require("../../utils/duration.util");
const { recomputeDurations, recomputeUnitDuration, recomputeCourseDuration } = require("../../utils/duration.util");
const { syncObjectivesCreate, syncObjectivesUpdate } = require("../../utils/courses/objectives.util");
const { archiveOne, archiveMany } = require("../../utils/courses/archive.util");
const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
const { permanentDeleteOne, permanentDeleteMany } = require("../../utils/courses/permanentDelete.util");
const { getFieldValues } = require("../../utils/fieldValues.util");
const { nextOrderIndex } = require("../../utils/courses/hierarchy.util");
const logActivity = require("../../utils/logActivity.util");
// ── Models ────────────────────────────────────────────────────────────────────
const {
Unit, Lesson, LessonPage,
CourseUnit, UnitLesson,
LessonObjective,
CourseAssessment, UnitQuiz,
QuizQuestion, QuizOption,
} = require("../../models/courses/courses.associations");
const mdl_Users = require("../../models/users/users.mdl");
@@ -23,65 +41,92 @@ const mdl_Users = require("../../models/users/users.mdl");
const notDeleted = { deletedAt: null };
const onlyDeleted = { deletedAt: { [Op.not]: null } };
// ── Helpers ───────────────────────────────────────────────────────────────────
async function resolveQuestionParent(params) {
const { quizId, assessmentId } = params;
if (quizId) {
const rec = await UnitQuiz.findOne({ where: { quiz_id: quizId, ...notDeleted } });
return { parentField: "quiz_id", parentId: quizId, parentRecord: rec };
// Refresh every unit this lesson is attached to + the courses above them.
async function recomputeParentDurations(lessonId) {
const links = await UnitLesson.findAll({ where: { lesson_id: lessonId }, attributes: ["unit_id"] });
const unitIds = [...new Set(links.map((l) => String(l.unit_id)))];
for (const unitId of unitIds) await recomputeUnitDuration(unitId);
if (unitIds.length) {
const courseLinks = await CourseUnit.findAll({ where: { unit_id: unitIds }, attributes: ["course_id"] });
for (const courseId of new Set(courseLinks.map((l) => String(l.course_id)))) {
await recomputeCourseDuration(courseId);
}
}
if (assessmentId) {
const rec = await CourseAssessment.findOne({ where: { assessment_id: assessmentId, ...notDeleted } });
return { parentField: "assessment_id", parentId: assessmentId, parentRecord: rec };
}
return null;
}
const LESSON_LIST_COMPUTED = [
{
key: "unit_count",
label: "Used in units",
type: "number",
literal: `(
SELECT CAST(COUNT(*) AS INTEGER)
FROM unit_lessons ul
JOIN units u ON u.unit_id = ul.unit_id AND u."deletedAt" IS NULL
WHERE ul.lesson_id = "Lesson"."lesson_id"
)`,
},
];
// ══════════════════════════════════════════════════════════════════════════════
// LESSON
// LESSON LIBRARY
// ══════════════════════════════════════════════════════════════════════════════
exports.getLessons = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted } });
if (!unit) return R.error(res, "Unit not found.", 404);
const result = await paginate(Lesson, req, {
auditOptions: { mdl_Users, parentAlias: "Lesson" },
context: "list",
computedAttributes: LESSON_LIST_COMPUTED,
findOptions: {
where: { unit_id: unitId, ...notDeleted },
order: [["order_index", "ASC"]],
where: { ...notDeleted },
order: [["createdAt", "DESC"]],
},
});
return R.success(res, "Lessons retrieved.", result);
} catch (err) {
console.error("[LESSON][GET ALL]", err);
console.error("[LESSON LIB][GET ALL]", err);
return R.error(res, "Could not retrieve lessons.", 500);
}
};
// Lightweight list for attach pickers
exports.getLessonsFlat = async (req, res) => {
try {
const rows = await sequelize.query(`
SELECT
l.lesson_id, l.uuid, l.title, l.description, l.duration_seconds,
(SELECT CAST(COUNT(*) AS INTEGER) FROM unit_lessons ul
JOIN units u ON u.unit_id = ul.unit_id AND u."deletedAt" IS NULL
WHERE ul.lesson_id = l.lesson_id) AS unit_count
FROM lessons l
WHERE l."deletedAt" IS NULL
ORDER BY l.title ASC
`, { type: sequelize.QueryTypes.SELECT });
return R.success(res, "Lessons retrieved.", rows);
} catch (err) {
console.error("[LESSON LIB][GET FLAT]", err);
return R.error(res, "Could not retrieve lessons.", 500);
}
};
exports.getLesson = async (req, res) => {
try {
const { courseId, unitId, lessonId } = req.params;
const { lessonId } = req.params;
const lesson = await Lesson.findOne({
where: { lesson_id: lessonId, unit_id: unitId, ...notDeleted },
where: { lesson_id: lessonId, ...notDeleted },
include: [
{ model: LessonPage, as: "page", required: false },
{ model: LessonObjective, as: "objectives", required: false, order: [["order_index", "ASC"]] },
{ model: Unit, as: "unit", where: { course_id: courseId, ...notDeleted } },
{ model: Unit, as: "units", where: notDeleted, required: false, attributes: ["unit_id", "uuid", "title"], through: { attributes: ["order_index"] } },
],
});
if (!lesson) return R.error(res, "Lesson not found.", 404);
return R.success(res, "Lesson retrieved.", { data: lesson.toJSON() });
} catch (err) {
console.error("[LESSON][GET ONE]", err);
console.error("[LESSON LIB][GET ONE]", err);
return R.error(res, "Could not retrieve lesson.", 500);
}
};
@@ -89,36 +134,46 @@ exports.getLesson = async (req, res) => {
exports.createLesson = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId } = req.params;
const { title, description, order, objectives = [], createdBy } = req.body;
const { title, description, unit_id, order, objectives = [], createdBy } = req.body;
if (!title) return R.error(res, "Title is required.", 400);
const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted } });
if (!unit) return R.error(res, "Unit not found.", 404);
const lesson = await Lesson.create({
unit_id: unitId,
title,
description: description ?? null,
order_index: order ?? 0,
duration_seconds: 0,
createdBy: createdBy ?? null,
createdBy: createdBy ?? req.user?.user_id ?? null,
}, { transaction: t });
await LessonPage.create({
lesson_id: lesson.lesson_id,
blocks: [],
createdBy: createdBy ?? null,
createdBy: createdBy ?? req.user?.user_id ?? null,
}, { transaction: t });
await syncObjectivesCreate(LessonObjective, "lesson_id", lesson.lesson_id, objectives, t);
// Optional immediate attach — lets the unit editor create-and-attach in one call
if (unit_id) {
const unit = await Unit.findOne({ where: { unit_id, ...notDeleted }, transaction: t });
if (!unit) {
await t.rollback();
return R.error(res, "Unit not found.", 404);
}
const order_index = order ?? await nextOrderIndex(UnitLesson, { unit_id }, t);
await UnitLesson.create({
unit_id,
lesson_id: lesson.lesson_id,
order_index,
createdBy: createdBy ?? req.user?.user_id ?? null,
}, { transaction: t });
}
await t.commit();
logActivity(req.user?.user_id, "create_lesson", { entityType: "lesson", entityId: lesson.lesson_id, details: { title: lesson.title, attached_unit_id: unit_id ?? null } });
return R.success(res, "Lesson created.", { data: lesson }, 201);
} catch (err) {
await t.rollback();
console.error("[LESSON][CREATE]", err);
console.error("[LESSON LIB][CREATE]", err);
return R.error(res, "Could not create lesson.", 500);
}
};
@@ -126,20 +181,16 @@ exports.createLesson = async (req, res) => {
exports.updateLesson = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId, lessonId } = req.params;
const { lessonId } = req.params;
const lesson = await Lesson.findOne({
where: { lesson_id: lessonId, unit_id: unitId, ...notDeleted },
include: [{ model: Unit, as: "unit", where: { course_id: courseId, ...notDeleted } }],
});
const lesson = await Lesson.findOne({ where: { lesson_id: lessonId, ...notDeleted } });
if (!lesson) return R.error(res, "Lesson not found.", 404);
const { title, description, order, objectives, updatedBy } = req.body;
const { title, description, objectives, updatedBy } = req.body;
if (title !== undefined) lesson.title = title;
if (description !== undefined) lesson.description = description;
if (order !== undefined) lesson.order_index = order;
lesson.updatedBy = updatedBy ?? null;
lesson.updatedBy = updatedBy ?? req.user?.user_id ?? null;
await lesson.save({ transaction: t });
if (objectives !== undefined) {
@@ -153,33 +204,28 @@ exports.updateLesson = async (req, res) => {
include: [{ model: LessonObjective, as: "objectives", order: [["order_index", "ASC"]] }],
});
logActivity(req.user?.user_id, "update_lesson", { entityType: "lesson", entityId: Number(lessonId) });
return R.success(res, "Lesson updated.", { data: updated });
} catch (err) {
await t.rollback();
console.error("[LESSON][UPDATE]", err);
console.error("[LESSON LIB][UPDATE]", err);
return R.error(res, "Could not update lesson.", 500);
}
};
exports.deleteLesson = async (req, res) => {
exports.archiveLesson = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId, lessonId } = req.params;
const lesson = await Lesson.findOne({
where: { lesson_id: lessonId, unit_id: unitId, ...notDeleted },
include: [{ model: Unit, as: "unit", where: { course_id: courseId, ...notDeleted } }],
});
if (!lesson) return R.error(res, "Lesson not found.", 404);
await lesson.update({ deletedBy: req.body.deletedBy ?? null }, { transaction: t });
await lesson.destroy({ transaction: t });
const { lessonId } = req.params;
const record = await archiveOne(Lesson, { lesson_id: lessonId, ...notDeleted }, req.user.user_id, t);
if (!record) return R.error(res, "Lesson not found.", 404);
await t.commit();
try { await recomputeParentDurations(lessonId); } catch (durErr) { console.error("[LESSON LIB][ARCHIVE][DURATION]", durErr); }
logActivity(req.user.user_id, "archive_lesson", { entityType: "lesson", entityId: Number(lessonId) });
return R.success(res, "Lesson archived.");
} catch (err) {
await t.rollback();
console.error("[LESSON][DELETE]", err);
console.error("[LESSON LIB][ARCHIVE]", err);
return R.error(res, "Could not archive lesson.", 500);
}
};
@@ -187,71 +233,55 @@ exports.deleteLesson = async (req, res) => {
exports.bulkArchiveLessons = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId } = req.params;
const { ids = [], deletedBy } = req.body;
const { ids = [] } = req.body;
if (!ids.length) return R.error(res, "No IDs provided.", 400);
const lessons = await Lesson.findAll({
where: { lesson_id: ids, unit_id: unitId, ...notDeleted },
include: [{ model: Unit, as: "unit", where: { course_id: courseId, ...notDeleted } }],
});
const lessons = await Lesson.findAll({ where: { lesson_id: ids, ...notDeleted } });
const validIds = lessons.map((l) => l.lesson_id);
const count = await archiveMany(Lesson, "lesson_id", validIds, deletedBy, t);
const count = await archiveMany(Lesson, "lesson_id", validIds, req.user.user_id, t);
await t.commit();
for (const id of validIds) {
try { await recomputeParentDurations(id); } catch (durErr) { console.error("[LESSON LIB][BULK ARCHIVE][DURATION]", durErr); }
}
logActivity(req.user.user_id, "bulk_archive_lessons", { entityType: "lesson", details: { ids: validIds, count } });
return R.success(res, `${count} lesson${count !== 1 ? "s" : ""} archived.`);
} catch (err) {
await t.rollback();
console.error("[LESSON][BULK ARCHIVE]", err);
console.error("[LESSON LIB][BULK ARCHIVE]", err);
return R.error(res, "Could not archive lessons.", 500);
}
};
exports.getArchivedLessons = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId },
paranoid: false,
});
if (!unit) return R.error(res, "Unit not found.", 404);
const result = await paginate(Lesson, req, {
auditOptions: { mdl_Users, parentAlias: "Lesson" },
context: "list",
context: "archived",
findOptions: {
where: { unit_id: unitId, ...onlyDeleted },
where: { ...onlyDeleted },
paranoid: false,
order: [["order_index", "ASC"]],
order: [["deletedAt", "DESC"]],
},
});
return R.success(res, "Archived lessons retrieved.", result);
} catch (err) {
console.error("[LESSON][GET ARCHIVES]", err);
console.error("[LESSON LIB][GET ARCHIVES]", err);
return R.error(res, "Could not retrieve archived lessons.", 500);
}
};
exports.getArchivedLesson = async (req, res) => {
try {
const { courseId, unitId, lessonId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId },
paranoid: false,
});
if (!unit) return R.error(res, "Unit not found.", 404);
const { lessonId } = req.params;
const lesson = await Lesson.findOne({
where: { lesson_id: lessonId, unit_id: unitId, ...onlyDeleted },
where: { lesson_id: lessonId, ...onlyDeleted },
paranoid: false,
});
if (!lesson) return R.error(res, "Archived lesson not found.", 404);
return R.success(res, "Archived lesson retrieved.", { data: lesson.toJSON() });
} catch (err) {
console.error("[LESSON][GET ARCHIVE ONE]", err);
console.error("[LESSON LIB][GET ARCHIVE ONE]", err);
return R.error(res, "Could not retrieve archived lesson.", 500);
}
};
@@ -259,26 +289,16 @@ exports.getArchivedLesson = async (req, res) => {
exports.restoreLesson = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId, lessonId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId },
paranoid: false,
});
if (!unit) return R.error(res, "Unit not found.", 404);
const record = await restoreOne(
Lesson,
{ lesson_id: lessonId, unit_id: unitId, ...onlyDeleted },
req.body.restoredBy,
t
);
const { lessonId } = req.params;
const record = await restoreOne(Lesson, { lesson_id: lessonId, ...onlyDeleted }, req.user.user_id, t);
if (!record) return R.error(res, "Archived lesson not found.", 404);
await t.commit();
try { await recomputeParentDurations(lessonId); } catch (durErr) { console.error("[LESSON LIB][RESTORE][DURATION]", durErr); }
logActivity(req.user.user_id, "restore_lesson", { entityType: "lesson", entityId: Number(lessonId) });
return R.success(res, "Lesson restored.", { data: record });
} catch (err) {
await t.rollback();
console.error("[LESSON][RESTORE]", err);
console.error("[LESSON LIB][RESTORE]", err);
return R.error(res, "Could not restore lesson.", 500);
}
};
@@ -286,34 +306,83 @@ exports.restoreLesson = async (req, res) => {
exports.bulkRestoreLessons = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId } = req.params;
const { ids = [], restoredBy } = req.body;
const { ids = [] } = req.body;
if (!ids.length) return R.error(res, "No IDs provided.", 400);
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId },
paranoid: false,
});
if (!unit) return R.error(res, "Unit not found.", 404);
const lessons = await Lesson.findAll({
where: { lesson_id: ids, unit_id: unitId, ...onlyDeleted },
paranoid: false,
});
const lessons = await Lesson.findAll({ where: { lesson_id: ids, ...onlyDeleted }, paranoid: false });
const validIds = lessons.map((l) => l.lesson_id);
const count = await restoreMany(Lesson, "lesson_id", validIds, restoredBy, t);
const count = await restoreMany(Lesson, "lesson_id", validIds, req.user.user_id, t);
await t.commit();
for (const id of validIds) {
try { await recomputeParentDurations(id); } catch (durErr) { console.error("[LESSON LIB][BULK RESTORE][DURATION]", durErr); }
}
logActivity(req.user.user_id, "bulk_restore_lessons", { entityType: "lesson", details: { ids: validIds, count } });
return R.success(res, `${count} lesson${count !== 1 ? "s" : ""} restored.`);
} catch (err) {
await t.rollback();
console.error("[LESSON][BULK RESTORE]", err);
console.error("[LESSON LIB][BULK RESTORE]", err);
return R.error(res, "Could not restore lessons.", 500);
}
};
exports.getLessonPermanentDeleteImpact = async (req, res) => {
try {
const { lessonId } = req.params;
const unitCount = await UnitLesson.count({ where: { lesson_id: lessonId } });
return R.success(res, "Impact retrieved.", { unitCount });
} catch (err) {
console.error("[LESSON LIB][PERMANENT DELETE IMPACT]", err);
return R.error(res, "Could not retrieve impact.", 500);
}
};
exports.permanentlyDeleteLesson = async (req, res) => {
const t = await sequelize.transaction();
try {
const { lessonId } = req.params;
const record = await permanentDeleteOne(Lesson, { lesson_id: lessonId }, t);
if (record === null) return R.error(res, "Lesson not found.", 404);
if (record === false) return R.error(res, "Lesson must be archived before it can be permanently deleted.", 400);
await UnitLesson.destroy({ where: { lesson_id: lessonId }, transaction: t });
await t.commit();
logActivity(req.user.user_id, "permanently_delete_lesson", { entityType: "lesson", entityId: Number(lessonId) });
return R.success(res, "Lesson permanently deleted.");
} catch (err) {
await t.rollback();
console.error("[LESSON LIB][PERMANENT DELETE]", err);
return R.error(res, "Could not permanently delete lesson.", 500);
}
};
exports.bulkPermanentlyDeleteLessons = async (req, res) => {
const t = await sequelize.transaction();
try {
const { ids = [] } = req.body;
if (!ids.length) return R.error(res, "No IDs provided.", 400);
const lessons = await Lesson.findAll({ where: { lesson_id: ids }, paranoid: false });
const validIds = lessons.map((l) => l.lesson_id);
const count = await permanentDeleteMany(Lesson, "lesson_id", validIds, t);
await UnitLesson.destroy({ where: { lesson_id: validIds }, transaction: t });
await t.commit();
logActivity(req.user.user_id, "bulk_permanently_delete_lessons", { entityType: "lesson", details: { ids: validIds, count } });
return R.success(res, `${count} lesson${count !== 1 ? "s" : ""} permanently deleted.`);
} catch (err) {
await t.rollback();
console.error("[LESSON LIB][BULK PERMANENT DELETE]", err);
return R.error(res, "Could not permanently delete lessons.", 500);
}
};
exports.getLessonFieldValues = getFieldValues(Lesson, "LESSON");
// ══════════════════════════════════════════════════════════════════════════════
// LESSON PAGE
// LESSON PAGE (same contract as before — keyed by lessonId only)
// ══════════════════════════════════════════════════════════════════════════════
exports.getLessonPage = async (req, res) => {
@@ -323,7 +392,7 @@ exports.getLessonPage = async (req, res) => {
if (!page) return R.error(res, "Lesson page not found.", 404);
return R.success(res, "Lesson page retrieved.", { data: page });
} catch (err) {
console.error("[LESSON PAGE][GET]", err);
console.error("[LESSON LIB][PAGE][GET]", err);
return R.error(res, "Could not retrieve lesson page.", 500);
}
};
@@ -341,16 +410,17 @@ exports.upsertLessonPage = async (req, res) => {
const [page, created] = await LessonPage.upsert({
lesson_id: lessonId,
blocks,
updatedBy: req.body.updatedBy ?? null,
createdBy: req.body.updatedBy ?? null,
updatedBy: req.body.updatedBy ?? req.user?.user_id ?? null,
createdBy: req.body.updatedBy ?? req.user?.user_id ?? null,
}, { returning: true });
try {
await recomputeDurations(lessonId);
} catch (durErr) {
console.error("[LESSON PAGE][DURATION]", durErr);
console.error("[LESSON LIB][PAGE][DURATION]", durErr);
}
logActivity(req.user?.user_id, "upsert_lesson_page", { entityType: "lesson", entityId: Number(lessonId) });
return R.success(
res,
created ? "Lesson page created." : "Lesson page updated.",
@@ -358,153 +428,7 @@ exports.upsertLessonPage = async (req, res) => {
created ? 201 : 200,
);
} catch (err) {
console.error("[LESSON PAGE][UPSERT]", err);
console.error("[LESSON LIB][PAGE][UPSERT]", err);
return R.error(res, "Could not save lesson page.", 500);
}
};
// ══════════════════════════════════════════════════════════════════════════════
// QUIZ QUESTIONS
// ══════════════════════════════════════════════════════════════════════════════
exports.getQuestions = async (req, res) => {
try {
const parent = await resolveQuestionParent(req.params);
if (!parent?.parentRecord) return R.error(res, "Parent not found.", 404);
const questions = await QuizQuestion.findAll({
where: { [parent.parentField]: parent.parentId, ...notDeleted },
order: [["order_index", "ASC"]],
include: [{ model: QuizOption, as: "options", order: [["order_index", "ASC"]] }],
});
return R.success(res, "Questions retrieved.", { data: questions });
} catch (err) {
console.error("[QUESTION][GET ALL]", err);
return R.error(res, "Could not retrieve questions.", 500);
}
};
exports.createQuestion = async (req, res) => {
const t = await sequelize.transaction();
try {
const parent = await resolveQuestionParent(req.params);
if (!parent?.parentRecord) return R.error(res, "Parent not found.", 404);
const { type, question, explanation, order_index, points, options = [], createdBy } = req.body;
const VALID_TYPES = ["true_false", "multiple_choice", "multi_select"];
if (!VALID_TYPES.includes(type)) return R.error(res, `Invalid type. Must be one of: ${VALID_TYPES.join(", ")}`, 400);
if (!question) return R.error(res, "question text is required.", 400);
let finalOptions = options;
if (type === "true_false" && !options.length) {
finalOptions = [
{ text: "True", is_correct: true, order_index: 0 },
{ text: "False", is_correct: false, order_index: 1 },
];
}
const q = await QuizQuestion.create({
[parent.parentField]: parent.parentId,
type,
question,
explanation: explanation ?? null,
order_index: order_index ?? 0,
points: points ?? 1,
createdBy: createdBy ?? null,
}, { transaction: t });
if (finalOptions.length) {
await QuizOption.bulkCreate(
finalOptions.map((o, i) => ({
question_id: q.question_id,
text: o.text,
is_correct: o.is_correct ?? false,
order_index: o.order_index ?? i,
})),
{ transaction: t }
);
}
await t.commit();
const created = await QuizQuestion.findOne({
where: { question_id: q.question_id },
include: [{ model: QuizOption, as: "options", order: [["order_index", "ASC"]] }],
});
return R.success(res, "Question created.", { data: created }, 201);
} catch (err) {
await t.rollback();
console.error("[QUESTION][CREATE]", err);
return R.error(res, "Could not create question.", 500);
}
};
exports.updateQuestion = async (req, res) => {
const t = await sequelize.transaction();
try {
const { questionId } = req.params;
const q = await QuizQuestion.findOne({ where: { question_id: questionId, ...notDeleted } });
if (!q) return R.error(res, "Question not found.", 404);
const { type, question, explanation, order_index, points, options, updatedBy } = req.body;
if (type !== undefined) q.type = type;
if (question !== undefined) q.question = question;
if (explanation !== undefined) q.explanation = explanation;
if (order_index !== undefined) q.order_index = order_index;
if (points !== undefined) q.points = points;
q.updatedBy = updatedBy ?? null;
await q.save({ transaction: t });
if (options !== undefined) {
await QuizOption.destroy({ where: { question_id: questionId }, transaction: t });
if (options.length) {
await QuizOption.bulkCreate(
options.map((o, i) => ({
question_id: questionId,
text: o.text,
is_correct: o.is_correct ?? false,
order_index: o.order_index ?? i,
})),
{ transaction: t }
);
}
}
await t.commit();
const updated = await QuizQuestion.findOne({
where: { question_id: questionId },
include: [{ model: QuizOption, as: "options", order: [["order_index", "ASC"]] }],
});
return R.success(res, "Question updated.", { data: updated });
} catch (err) {
await t.rollback();
console.error("[QUESTION][UPDATE]", err);
return R.error(res, "Could not update question.", 500);
}
};
exports.deleteQuestion = async (req, res) => {
const t = await sequelize.transaction();
try {
const { questionId } = req.params;
const record = await archiveOne(
QuizQuestion,
{ question_id: questionId, ...notDeleted },
req.body.deletedBy,
t
);
if (!record) return R.error(res, "Question not found.", 404);
await t.commit();
return R.success(res, "Question archived.");
} catch (err) {
await t.rollback();
console.error("[QUESTION][DELETE]", err);
return R.error(res, "Could not archive question.", 500);
}
};