mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -17,8 +17,9 @@
|
||||
const { Op } = require('sequelize');
|
||||
const R = require('../../utils/response.util');
|
||||
const CourseReadingProgress = require('../../models/courses/course_reading_progress.mdl');
|
||||
const { Course, Unit, Lesson } = require('../../models/courses/courses.associations');
|
||||
const { Course, Unit, Lesson, CourseUnit } = require('../../models/courses/courses.associations');
|
||||
const mdl_Users = require('../../models/users/users.mdl');
|
||||
const { countCourseLessons, getCourseUnitIds, flattenUnits } = require('../../utils/courses/hierarchy.util');
|
||||
|
||||
const notDeleted = { deletedAt: null };
|
||||
|
||||
@@ -40,27 +41,13 @@ exports.getCourseReadingProgress = async (req, res) => {
|
||||
});
|
||||
if (!course) return R.error(res, 'Course not found.', 404);
|
||||
|
||||
// Count total lessons and units in the course (structure totals)
|
||||
const [units, allLessons] = await Promise.all([
|
||||
Unit.findAll({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['unit_id', 'uuid'],
|
||||
}),
|
||||
Lesson.findAll({
|
||||
include: [{
|
||||
model: Unit,
|
||||
as: 'unit',
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: [],
|
||||
required: true,
|
||||
}],
|
||||
where: { ...notDeleted },
|
||||
attributes: ['lesson_id', 'uuid'],
|
||||
}),
|
||||
// Count total lessons and units in the course (structure totals via junctions)
|
||||
const [unitIds, lessons_total] = await Promise.all([
|
||||
getCourseUnitIds(courseId),
|
||||
countCourseLessons(courseId),
|
||||
]);
|
||||
|
||||
const units_total = units.length;
|
||||
const lessons_total = allLessons.length;
|
||||
const units_total = unitIds.length;
|
||||
|
||||
// All progress rows for this course, grouped per user
|
||||
const rows = await CourseReadingProgress.findAll({
|
||||
@@ -147,20 +134,26 @@ exports.getUserReadingProgress = async (req, res) => {
|
||||
try {
|
||||
const { courseId, userId } = req.params;
|
||||
|
||||
const [units, progressRows] = await Promise.all([
|
||||
const [unitRows, progressRows] = await Promise.all([
|
||||
Unit.findAll({
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
attributes: ['unit_id', 'uuid', 'title', 'order_index'],
|
||||
include: [{
|
||||
model: Lesson,
|
||||
as: 'lessons',
|
||||
where: notDeleted,
|
||||
required: false,
|
||||
attributes: ['lesson_id', 'uuid', 'title', 'order_index'],
|
||||
}],
|
||||
order: [
|
||||
['order_index', 'ASC'],
|
||||
[{ model: Lesson, as: 'lessons' }, 'order_index', 'ASC'],
|
||||
where: notDeleted,
|
||||
attributes: ['unit_id', 'uuid', 'title'],
|
||||
include: [
|
||||
{
|
||||
model: CourseUnit,
|
||||
as: 'courseLinks',
|
||||
where: { course_id: courseId },
|
||||
required: true,
|
||||
attributes: ['order_index'],
|
||||
},
|
||||
{
|
||||
model: Lesson,
|
||||
as: 'lessons',
|
||||
where: notDeleted,
|
||||
required: false,
|
||||
attributes: ['lesson_id', 'uuid', 'title'],
|
||||
through: { attributes: ['order_index'] },
|
||||
},
|
||||
],
|
||||
}),
|
||||
CourseReadingProgress.findAll({
|
||||
@@ -169,6 +162,14 @@ exports.getUserReadingProgress = async (req, res) => {
|
||||
}),
|
||||
]);
|
||||
|
||||
// Sort by junction order (course-level, then unit-level for lessons)
|
||||
const units = flattenUnits(unitRows.map((u) => {
|
||||
const plain = u.toJSON();
|
||||
plain.CourseUnit = { order_index: plain.courseLinks?.[0]?.order_index ?? 0 };
|
||||
delete plain.courseLinks;
|
||||
return plain;
|
||||
}));
|
||||
|
||||
// Build a quick lookup: { [reference_id (uuid)]: status }
|
||||
const progressMap = Object.fromEntries(
|
||||
progressRows.map((r) => [r.reference_id, { status: r.status, completed_at: r.completed_at }])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -1,17 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: units.controller.js (admin)
|
||||
* Type of Program: Controller
|
||||
* Description: Standalone Unit library — Units live independently of Courses.
|
||||
*
|
||||
* /admin/units → library CRUD (list / create / update / archive / restore / permanent delete)
|
||||
* /admin/units/:unitId/lessons → attach / detach / reorder standalone Lessons on this Unit
|
||||
* /admin/units/:unitId/quiz → the Unit's quiz (travels with the Unit into every course it's attached to)
|
||||
*
|
||||
* Membership in a course is a course_units row (managed from the course builder);
|
||||
* archiving here removes the Unit from every course view 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 { 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 { flattenLessons, nextOrderIndex, reorderJunction } = require("../../utils/courses/hierarchy.util");
|
||||
const { recomputeUnitDuration, recomputeCourseDuration } = require("../../utils/duration.util");
|
||||
const logActivity = require("../../utils/logActivity.util");
|
||||
|
||||
// ── Models ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const {
|
||||
Course, Unit, Lesson,
|
||||
CourseUnit, UnitLesson,
|
||||
UnitQuiz, QuizQuestion, QuizOption,
|
||||
UnitReadingProgress, LessonReadingProgress,
|
||||
} = require("../../models/courses/courses.associations");
|
||||
|
||||
const mdl_Users = require("../../models/users/users.mdl");
|
||||
@@ -19,126 +42,205 @@ const mdl_Users = require("../../models/users/users.mdl");
|
||||
const notDeleted = { deletedAt: null };
|
||||
const onlyDeleted = { deletedAt: { [Op.not]: null } };
|
||||
|
||||
// Refresh duration of every course this unit is attached to (post-archive/restore).
|
||||
async function recomputeParentCourseDurations(unitId) {
|
||||
const links = await CourseUnit.findAll({ where: { unit_id: unitId }, attributes: ["course_id"] });
|
||||
for (const courseId of new Set(links.map((l) => String(l.course_id)))) {
|
||||
await recomputeCourseDuration(courseId);
|
||||
}
|
||||
}
|
||||
|
||||
const UNIT_LIST_COMPUTED = [
|
||||
{
|
||||
key: "quiz_id",
|
||||
label: "Quiz ID",
|
||||
type: "text",
|
||||
literal: `(SELECT quiz_id FROM unit_quizzes WHERE unit_id = "Unit"."unit_id" AND "deletedAt" IS NULL LIMIT 1)`,
|
||||
},
|
||||
{
|
||||
key: "lesson_count",
|
||||
label: "Lessons",
|
||||
type: "number",
|
||||
literal: `(
|
||||
SELECT CAST(COUNT(*) AS INTEGER)
|
||||
FROM unit_lessons ul
|
||||
JOIN lessons l ON l.lesson_id = ul.lesson_id AND l."deletedAt" IS NULL
|
||||
WHERE ul.unit_id = "Unit"."unit_id"
|
||||
)`,
|
||||
},
|
||||
{
|
||||
key: "course_count",
|
||||
label: "Used in courses",
|
||||
type: "number",
|
||||
literal: `(
|
||||
SELECT CAST(COUNT(*) AS INTEGER)
|
||||
FROM course_units cu
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE cu.unit_id = "Unit"."unit_id"
|
||||
)`,
|
||||
},
|
||||
];
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// UNIT
|
||||
// UNIT LIBRARY
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
exports.getUnits = async (req, res) => {
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
|
||||
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
|
||||
if (!course) return R.error(res, "Course not found.", 404);
|
||||
|
||||
const result = await paginate(Unit, req, {
|
||||
auditOptions: { mdl_Users, parentAlias: "Unit" },
|
||||
context: "list",
|
||||
computedAttributes: UNIT_LIST_COMPUTED,
|
||||
findOptions: {
|
||||
where: { course_id: courseId, ...notDeleted },
|
||||
order: [["order_index", "ASC"]],
|
||||
where: { ...notDeleted },
|
||||
order: [["createdAt", "DESC"]],
|
||||
},
|
||||
computedAttributes: [
|
||||
{
|
||||
key: "quiz_id",
|
||||
label: "Quiz ID",
|
||||
type: "text",
|
||||
literal: `(SELECT quiz_id FROM unit_quizzes WHERE unit_id = "Unit"."unit_id" AND "deletedAt" IS NULL LIMIT 1)`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return R.success(res, "Units retrieved.", result);
|
||||
} catch (err) {
|
||||
console.error("[UNIT][GET ALL]", err);
|
||||
console.error("[UNIT LIB][GET ALL]", err);
|
||||
return R.error(res, "Could not retrieve units.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Lightweight list for attach pickers: { unit_id, uuid, title, lesson_count, course_count }
|
||||
exports.getUnitsFlat = async (req, res) => {
|
||||
try {
|
||||
const rows = await sequelize.query(`
|
||||
SELECT
|
||||
u.unit_id, u.uuid, u.title, u.description, u.duration_seconds,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM unit_lessons ul
|
||||
JOIN lessons l ON l.lesson_id = ul.lesson_id AND l."deletedAt" IS NULL
|
||||
WHERE ul.unit_id = u.unit_id) AS lesson_count,
|
||||
(SELECT CAST(COUNT(*) AS INTEGER) FROM course_units cu
|
||||
JOIN courses c ON c.course_id = cu.course_id AND c."deletedAt" IS NULL
|
||||
WHERE cu.unit_id = u.unit_id) AS course_count
|
||||
FROM units u
|
||||
WHERE u."deletedAt" IS NULL
|
||||
ORDER BY u.title ASC
|
||||
`, { type: sequelize.QueryTypes.SELECT });
|
||||
return R.success(res, "Units retrieved.", rows);
|
||||
} catch (err) {
|
||||
console.error("[UNIT LIB][GET FLAT]", err);
|
||||
return R.error(res, "Could not retrieve units.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUnit = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const { unitId } = req.params;
|
||||
|
||||
const unit = await Unit.findOne({
|
||||
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
|
||||
where: { unit_id: unitId, ...notDeleted },
|
||||
include: [
|
||||
{ model: Lesson, as: "lessons", where: notDeleted, required: false },
|
||||
{ model: Lesson, as: "lessons", where: notDeleted, required: false, through: { attributes: ["order_index"] } },
|
||||
{ model: UnitQuiz, as: "quiz", required: false },
|
||||
{ model: Course, as: "courses", where: notDeleted, required: false, attributes: ["course_id", "uuid", "title", "subscription"], through: { attributes: ["order_index"] } },
|
||||
],
|
||||
order: [[{ model: Lesson, as: "lessons" }, "order_index", "ASC"]],
|
||||
});
|
||||
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
return R.success(res, "Unit retrieved.", { data: unit.toJSON() });
|
||||
|
||||
const plain = unit.toJSON();
|
||||
plain.lessons = flattenLessons(plain.lessons);
|
||||
return R.success(res, "Unit retrieved.", { data: plain });
|
||||
} catch (err) {
|
||||
console.error("[UNIT][GET ONE]", err);
|
||||
console.error("[UNIT LIB][GET ONE]", err);
|
||||
return R.error(res, "Could not retrieve unit.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.createUnit = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const { title, description, order, createdBy } = req.body;
|
||||
|
||||
const { title, description, course_id, order, createdBy } = req.body;
|
||||
if (!title) return R.error(res, "Title is required.", 400);
|
||||
|
||||
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
|
||||
if (!course) return R.error(res, "Course not found.", 404);
|
||||
|
||||
const unit = await Unit.create({
|
||||
course_id: courseId,
|
||||
title,
|
||||
description: description ?? null,
|
||||
order_index: order ?? 0,
|
||||
duration_seconds: 0,
|
||||
createdBy: createdBy ?? null,
|
||||
});
|
||||
createdBy: createdBy ?? req.user?.user_id ?? null,
|
||||
}, { transaction: t });
|
||||
|
||||
// Optional immediate attach — lets the course builder create-and-attach in one call
|
||||
if (course_id) {
|
||||
const course = await Course.findOne({ where: { course_id, ...notDeleted }, transaction: t });
|
||||
if (!course) {
|
||||
await t.rollback();
|
||||
return R.error(res, "Course not found.", 404);
|
||||
}
|
||||
const order_index = order ?? await nextOrderIndex(CourseUnit, { course_id }, t);
|
||||
await CourseUnit.create({
|
||||
course_id,
|
||||
unit_id: unit.unit_id,
|
||||
order_index,
|
||||
createdBy: createdBy ?? req.user?.user_id ?? null,
|
||||
}, { transaction: t });
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
logActivity(req.user?.user_id, "create_unit", { entityType: "unit", entityId: unit.unit_id, details: { title: unit.title, attached_course_id: course_id ?? null } });
|
||||
return R.success(res, "Unit created.", { data: unit }, 201);
|
||||
} catch (err) {
|
||||
console.error("[UNIT][CREATE]", err);
|
||||
await t.rollback();
|
||||
console.error("[UNIT LIB][CREATE]", err);
|
||||
return R.error(res, "Could not create unit.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateUnit = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const { unitId } = req.params;
|
||||
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted } });
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, ...notDeleted } });
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
|
||||
const { title, description, order, updatedBy } = req.body;
|
||||
const { title, description, updatedBy } = req.body;
|
||||
|
||||
if (title !== undefined) unit.title = title;
|
||||
if (description !== undefined) unit.description = description;
|
||||
if (order !== undefined) unit.order_index = order;
|
||||
unit.updatedBy = updatedBy ?? null;
|
||||
unit.updatedBy = updatedBy ?? req.user?.user_id ?? null;
|
||||
|
||||
await unit.save();
|
||||
logActivity(req.user?.user_id, "update_unit", { entityType: "unit", entityId: Number(unitId) });
|
||||
return R.success(res, "Unit updated.", { data: unit });
|
||||
} catch (err) {
|
||||
console.error("[UNIT][UPDATE]", err);
|
||||
console.error("[UNIT LIB][UPDATE]", err);
|
||||
return R.error(res, "Could not update unit.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteUnit = async (req, res) => {
|
||||
exports.getUnitArchiveImpact = async (req, res) => {
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
|
||||
const [completionCount, progressCount, courseCount] = await Promise.all([
|
||||
UnitReadingProgress.count({ where: { unit_id: unitId, status: "completed" } }),
|
||||
LessonReadingProgress.count({ where: { unit_id: unitId }, distinct: true, col: "user_id" }),
|
||||
CourseUnit.count({ where: { unit_id: unitId } }),
|
||||
]);
|
||||
|
||||
return R.success(res, "Impact retrieved.", { completionCount, progressCount, courseCount });
|
||||
} catch (err) {
|
||||
console.error("[UNIT LIB][ARCHIVE IMPACT]", err);
|
||||
return R.error(res, "Could not retrieve impact.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.archiveUnit = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const record = await archiveOne(
|
||||
Unit,
|
||||
{ unit_id: unitId, course_id: courseId, ...notDeleted },
|
||||
req.body.deletedBy,
|
||||
t
|
||||
);
|
||||
const { unitId } = req.params;
|
||||
const record = await archiveOne(Unit, { unit_id: unitId, ...notDeleted }, req.user.user_id, t);
|
||||
if (!record) return R.error(res, "Unit not found.", 404);
|
||||
await t.commit();
|
||||
try { await recomputeParentCourseDurations(unitId); } catch (durErr) { console.error("[UNIT LIB][ARCHIVE][DURATION]", durErr); }
|
||||
logActivity(req.user.user_id, "archive_unit", { entityType: "unit", entityId: Number(unitId) });
|
||||
return R.success(res, "Unit archived.");
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT][DELETE]", err);
|
||||
console.error("[UNIT LIB][ARCHIVE]", err);
|
||||
return R.error(res, "Could not archive unit.", 500);
|
||||
}
|
||||
};
|
||||
@@ -146,60 +248,55 @@ exports.deleteUnit = async (req, res) => {
|
||||
exports.bulkArchiveUnits = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const { ids = [], deletedBy } = req.body;
|
||||
const { ids = [] } = req.body;
|
||||
if (!ids.length) return R.error(res, "No IDs provided.", 400);
|
||||
|
||||
const units = await Unit.findAll({
|
||||
where: { unit_id: ids, course_id: courseId, ...notDeleted },
|
||||
});
|
||||
const units = await Unit.findAll({ where: { unit_id: ids, ...notDeleted } });
|
||||
const validIds = units.map((u) => u.unit_id);
|
||||
|
||||
const count = await archiveMany(Unit, "unit_id", validIds, deletedBy, t);
|
||||
const count = await archiveMany(Unit, "unit_id", validIds, req.user.user_id, t);
|
||||
await t.commit();
|
||||
for (const id of validIds) {
|
||||
try { await recomputeParentCourseDurations(id); } catch (durErr) { console.error("[UNIT LIB][BULK ARCHIVE][DURATION]", durErr); }
|
||||
}
|
||||
logActivity(req.user.user_id, "bulk_archive_units", { entityType: "unit", details: { ids: validIds, count } });
|
||||
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} archived.`);
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT][BULK ARCHIVE]", err);
|
||||
console.error("[UNIT LIB][BULK ARCHIVE]", err);
|
||||
return R.error(res, "Could not archive units.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getArchivedUnits = async (req, res) => {
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
|
||||
const course = await Course.findOne({ where: { course_id: courseId }, paranoid: false });
|
||||
if (!course) return R.error(res, "Course not found.", 404);
|
||||
|
||||
const result = await paginate(Unit, req, {
|
||||
auditOptions: { mdl_Users, parentAlias: "Unit" },
|
||||
context: "list",
|
||||
context: "archived",
|
||||
findOptions: {
|
||||
where: { course_id: courseId, ...onlyDeleted },
|
||||
where: { ...onlyDeleted },
|
||||
paranoid: false,
|
||||
order: [["order_index", "ASC"]],
|
||||
order: [["deletedAt", "DESC"]],
|
||||
},
|
||||
});
|
||||
|
||||
return R.success(res, "Archived units retrieved.", result);
|
||||
} catch (err) {
|
||||
console.error("[UNIT][GET ARCHIVES]", err);
|
||||
console.error("[UNIT LIB][GET ARCHIVES]", err);
|
||||
return R.error(res, "Could not retrieve archived units.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getArchivedUnit = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const { unitId } = req.params;
|
||||
const unit = await Unit.findOne({
|
||||
where: { unit_id: unitId, course_id: courseId, ...onlyDeleted },
|
||||
where: { unit_id: unitId, ...onlyDeleted },
|
||||
paranoid: false,
|
||||
});
|
||||
if (!unit) return R.error(res, "Archived unit not found.", 404);
|
||||
return R.success(res, "Archived unit retrieved.", { data: unit.toJSON() });
|
||||
} catch (err) {
|
||||
console.error("[UNIT][GET ARCHIVE ONE]", err);
|
||||
console.error("[UNIT LIB][GET ARCHIVE ONE]", err);
|
||||
return R.error(res, "Could not retrieve archived unit.", 500);
|
||||
}
|
||||
};
|
||||
@@ -207,19 +304,16 @@ exports.getArchivedUnit = async (req, res) => {
|
||||
exports.restoreUnit = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const record = await restoreOne(
|
||||
Unit,
|
||||
{ unit_id: unitId, course_id: courseId, ...onlyDeleted },
|
||||
req.body.restoredBy,
|
||||
t
|
||||
);
|
||||
const { unitId } = req.params;
|
||||
const record = await restoreOne(Unit, { unit_id: unitId, ...onlyDeleted }, req.user.user_id, t);
|
||||
if (!record) return R.error(res, "Archived unit not found.", 404);
|
||||
await t.commit();
|
||||
try { await recomputeParentCourseDurations(unitId); } catch (durErr) { console.error("[UNIT LIB][RESTORE][DURATION]", durErr); }
|
||||
logActivity(req.user.user_id, "restore_unit", { entityType: "unit", entityId: Number(unitId) });
|
||||
return R.success(res, "Unit restored.", { data: record });
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT][RESTORE]", err);
|
||||
console.error("[UNIT LIB][RESTORE]", err);
|
||||
return R.error(res, "Could not restore unit.", 500);
|
||||
}
|
||||
};
|
||||
@@ -227,35 +321,184 @@ exports.restoreUnit = async (req, res) => {
|
||||
exports.bulkRestoreUnits = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const { ids = [], restoredBy } = req.body;
|
||||
const { ids = [] } = req.body;
|
||||
if (!ids.length) return R.error(res, "No IDs provided.", 400);
|
||||
|
||||
const units = await Unit.findAll({
|
||||
where: { unit_id: ids, course_id: courseId, ...onlyDeleted },
|
||||
paranoid: false,
|
||||
});
|
||||
const units = await Unit.findAll({ where: { unit_id: ids, ...onlyDeleted }, paranoid: false });
|
||||
const validIds = units.map((u) => u.unit_id);
|
||||
|
||||
const count = await restoreMany(Unit, "unit_id", validIds, restoredBy, t);
|
||||
const count = await restoreMany(Unit, "unit_id", validIds, req.user.user_id, t);
|
||||
await t.commit();
|
||||
for (const id of validIds) {
|
||||
try { await recomputeParentCourseDurations(id); } catch (durErr) { console.error("[UNIT LIB][BULK RESTORE][DURATION]", durErr); }
|
||||
}
|
||||
logActivity(req.user.user_id, "bulk_restore_units", { entityType: "unit", details: { ids: validIds, count } });
|
||||
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} restored.`);
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT][BULK RESTORE]", err);
|
||||
console.error("[UNIT LIB][BULK RESTORE]", err);
|
||||
return R.error(res, "Could not restore units.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUnitPermanentDeleteImpact = async (req, res) => {
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
|
||||
const [lessonCount, courseCount] = await Promise.all([
|
||||
UnitLesson.count({ where: { unit_id: unitId } }),
|
||||
CourseUnit.count({ where: { unit_id: unitId } }),
|
||||
]);
|
||||
|
||||
return R.success(res, "Impact retrieved.", { lessonCount, courseCount });
|
||||
} catch (err) {
|
||||
console.error("[UNIT LIB][PERMANENT DELETE IMPACT]", err);
|
||||
return R.error(res, "Could not retrieve impact.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.permanentlyDeleteUnit = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
const record = await permanentDeleteOne(Unit, { unit_id: unitId }, t);
|
||||
if (record === null) return R.error(res, "Unit not found.", 404);
|
||||
if (record === false) return R.error(res, "Unit must be archived before it can be permanently deleted.", 400);
|
||||
|
||||
// Junction rows don't cascade from a paranoid destroy — clean them explicitly
|
||||
await CourseUnit.destroy({ where: { unit_id: unitId }, transaction: t });
|
||||
await UnitLesson.destroy({ where: { unit_id: unitId }, transaction: t });
|
||||
|
||||
await t.commit();
|
||||
logActivity(req.user.user_id, "permanently_delete_unit", { entityType: "unit", entityId: Number(unitId) });
|
||||
return R.success(res, "Unit permanently deleted.");
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT LIB][PERMANENT DELETE]", err);
|
||||
return R.error(res, "Could not permanently delete unit.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.bulkPermanentlyDeleteUnits = 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 units = await Unit.findAll({ where: { unit_id: ids }, paranoid: false });
|
||||
const validIds = units.map((u) => u.unit_id);
|
||||
|
||||
const count = await permanentDeleteMany(Unit, "unit_id", validIds, t);
|
||||
await CourseUnit.destroy({ where: { unit_id: validIds }, transaction: t });
|
||||
await UnitLesson.destroy({ where: { unit_id: validIds }, transaction: t });
|
||||
|
||||
await t.commit();
|
||||
logActivity(req.user.user_id, "bulk_permanently_delete_units", { entityType: "unit", details: { ids: validIds, count } });
|
||||
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} permanently deleted.`);
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT LIB][BULK PERMANENT DELETE]", err);
|
||||
return R.error(res, "Could not permanently delete units.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUnitFieldValues = getFieldValues(Unit, "UNIT");
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// UNIT QUIZ
|
||||
// UNIT ⇄ LESSON MEMBERSHIP (attach / detach / reorder)
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// POST /admin/units/:unitId/lessons { lesson_ids: [..] } — append existing lessons
|
||||
exports.attachLessons = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
const { lesson_ids = [] } = req.body;
|
||||
if (!lesson_ids.length) return R.error(res, "lesson_ids is required.", 400);
|
||||
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, ...notDeleted }, transaction: t });
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
|
||||
const lessons = await Lesson.findAll({ where: { lesson_id: lesson_ids, ...notDeleted }, transaction: t });
|
||||
if (lessons.length !== lesson_ids.length) {
|
||||
await t.rollback();
|
||||
return R.error(res, "One or more lessons were not found.", 404);
|
||||
}
|
||||
|
||||
const existing = await UnitLesson.findAll({ where: { unit_id: unitId, lesson_id: lesson_ids }, transaction: t });
|
||||
const existingSet = new Set(existing.map((r) => String(r.lesson_id)));
|
||||
const toAttach = lesson_ids.filter((id) => !existingSet.has(String(id)));
|
||||
|
||||
let order = await nextOrderIndex(UnitLesson, { unit_id: unitId }, t);
|
||||
await UnitLesson.bulkCreate(
|
||||
toAttach.map((lesson_id) => ({
|
||||
unit_id: unitId,
|
||||
lesson_id,
|
||||
order_index: order++,
|
||||
createdBy: req.user?.user_id ?? null,
|
||||
})),
|
||||
{ transaction: t }
|
||||
);
|
||||
|
||||
await t.commit();
|
||||
try { await recomputeUnitDuration(unitId); await recomputeParentCourseDurations(unitId); } catch (durErr) { console.error("[UNIT LIB][ATTACH LESSONS][DURATION]", durErr); }
|
||||
logActivity(req.user?.user_id, "attach_lessons", { entityType: "unit", entityId: Number(unitId), details: { lesson_ids: toAttach } });
|
||||
return R.success(res, `${toAttach.length} lesson${toAttach.length !== 1 ? "s" : ""} attached.`, { attached: toAttach, skipped: lesson_ids.filter((id) => existingSet.has(String(id))) });
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT LIB][ATTACH LESSONS]", err);
|
||||
return R.error(res, "Could not attach lessons.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /admin/units/:unitId/lessons/:lessonId — detach (lesson survives in the library)
|
||||
exports.detachLesson = async (req, res) => {
|
||||
try {
|
||||
const { unitId, lessonId } = req.params;
|
||||
|
||||
const removed = await UnitLesson.destroy({ where: { unit_id: unitId, lesson_id: lessonId } });
|
||||
if (!removed) return R.error(res, "Lesson is not attached to this unit.", 404);
|
||||
|
||||
try { await recomputeUnitDuration(unitId); await recomputeParentCourseDurations(unitId); } catch (durErr) { console.error("[UNIT LIB][DETACH LESSON][DURATION]", durErr); }
|
||||
logActivity(req.user?.user_id, "detach_lesson", { entityType: "unit", entityId: Number(unitId), details: { lesson_id: Number(lessonId) } });
|
||||
return R.success(res, "Lesson detached.");
|
||||
} catch (err) {
|
||||
console.error("[UNIT LIB][DETACH LESSON]", err);
|
||||
return R.error(res, "Could not detach lesson.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /admin/units/:unitId/lessons/order { lesson_ids: [orderedIds] }
|
||||
exports.reorderLessons = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
const { lesson_ids = [] } = req.body;
|
||||
if (!lesson_ids.length) return R.error(res, "lesson_ids is required.", 400);
|
||||
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, ...notDeleted }, transaction: t });
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
|
||||
await reorderJunction(UnitLesson, "unit_id", unitId, "lesson_id", lesson_ids, t);
|
||||
await t.commit();
|
||||
logActivity(req.user?.user_id, "reorder_lessons", { entityType: "unit", entityId: Number(unitId), details: { lesson_ids } });
|
||||
return R.success(res, "Lesson order updated.");
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT LIB][REORDER LESSONS]", err);
|
||||
return R.error(res, "Could not reorder lessons.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// UNIT QUIZ (1:1 with the Unit — travels with it into every attached course)
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
exports.getQuiz = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const { unitId } = req.params;
|
||||
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted } });
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, ...notDeleted } });
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
|
||||
const quiz = await UnitQuiz.findOne({
|
||||
@@ -270,54 +513,60 @@ exports.getQuiz = async (req, res) => {
|
||||
if (!quiz) return R.error(res, "Quiz not found.", 404);
|
||||
return R.success(res, "Quiz retrieved.", { data: quiz });
|
||||
} catch (err) {
|
||||
console.error("[QUIZ][GET]", err);
|
||||
console.error("[UNIT LIB][QUIZ][GET]", err);
|
||||
return R.error(res, "Could not retrieve quiz.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.createQuiz = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
const { title, is_required, passing_score, createdBy } = req.body;
|
||||
const { unitId } = req.params;
|
||||
const { title, is_required, passing_score, max_questions, shuffle_questions, createdBy } = req.body;
|
||||
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted } });
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId, ...notDeleted } });
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
|
||||
const existing = await UnitQuiz.findOne({ where: { unit_id: unitId, ...notDeleted } });
|
||||
if (existing) return R.error(res, "Quiz already exists for this unit.", 409);
|
||||
|
||||
const quiz = await UnitQuiz.create({
|
||||
unit_id: unitId,
|
||||
title: title ?? null,
|
||||
is_required: is_required ?? false,
|
||||
passing_score: passing_score ?? 70,
|
||||
createdBy: createdBy ?? null,
|
||||
unit_id: unitId,
|
||||
title: title ?? null,
|
||||
is_required: is_required ?? false,
|
||||
passing_score: passing_score ?? 70,
|
||||
max_questions: max_questions ?? null,
|
||||
shuffle_questions: shuffle_questions ?? false,
|
||||
createdBy: createdBy ?? req.user?.user_id ?? null,
|
||||
});
|
||||
|
||||
logActivity(req.user?.user_id, "create_quiz", { entityType: "quiz", entityId: quiz.quiz_id });
|
||||
return R.success(res, "Quiz created.", { data: quiz }, 201);
|
||||
} catch (err) {
|
||||
console.error("[QUIZ][CREATE]", err);
|
||||
console.error("[UNIT LIB][QUIZ][CREATE]", err);
|
||||
return R.error(res, "Could not create quiz.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateQuiz = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId, quizId } = req.params;
|
||||
const { title, is_required, passing_score, updatedBy } = req.body;
|
||||
const { unitId, quizId } = req.params;
|
||||
const { title, is_required, passing_score, max_questions, shuffle_questions, updatedBy } = req.body;
|
||||
|
||||
const quiz = await UnitQuiz.findOne({ where: { quiz_id: quizId, unit_id: unitId, ...notDeleted } });
|
||||
if (!quiz) return R.error(res, "Quiz not found.", 404);
|
||||
|
||||
if (title !== undefined) quiz.title = title;
|
||||
if (is_required !== undefined) quiz.is_required = is_required;
|
||||
if (passing_score !== undefined) quiz.passing_score = passing_score;
|
||||
quiz.updatedBy = updatedBy ?? null;
|
||||
if (title !== undefined) quiz.title = title;
|
||||
if (is_required !== undefined) quiz.is_required = is_required;
|
||||
if (passing_score !== undefined) quiz.passing_score = passing_score;
|
||||
if (max_questions !== undefined) quiz.max_questions = max_questions;
|
||||
if (shuffle_questions !== undefined) quiz.shuffle_questions = shuffle_questions;
|
||||
quiz.updatedBy = updatedBy ?? req.user?.user_id ?? null;
|
||||
|
||||
await quiz.save();
|
||||
logActivity(req.user?.user_id, "update_quiz", { entityType: "quiz", entityId: Number(quizId) });
|
||||
return R.success(res, "Quiz updated.", { data: quiz });
|
||||
} catch (err) {
|
||||
console.error("[QUIZ][UPDATE]", err);
|
||||
console.error("[UNIT LIB][QUIZ][UPDATE]", err);
|
||||
return R.error(res, "Could not update quiz.", 500);
|
||||
}
|
||||
};
|
||||
@@ -326,18 +575,49 @@ exports.deleteQuiz = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { unitId, quizId } = req.params;
|
||||
const record = await archiveOne(
|
||||
UnitQuiz,
|
||||
{ quiz_id: quizId, unit_id: unitId, ...notDeleted },
|
||||
req.body.deletedBy,
|
||||
t
|
||||
);
|
||||
const record = await archiveOne(UnitQuiz, { quiz_id: quizId, unit_id: unitId, ...notDeleted }, req.user?.user_id, t);
|
||||
if (!record) return R.error(res, "Quiz not found.", 404);
|
||||
await t.commit();
|
||||
logActivity(req.user?.user_id, "archive_quiz", { entityType: "quiz", entityId: Number(quizId) });
|
||||
return R.success(res, "Quiz archived.");
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[QUIZ][DELETE]", err);
|
||||
console.error("[UNIT LIB][QUIZ][ARCHIVE]", err);
|
||||
return R.error(res, "Could not archive quiz.", 500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.getArchivedQuiz = async (req, res) => {
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
|
||||
const unit = await Unit.findOne({ where: { unit_id: unitId }, paranoid: false });
|
||||
if (!unit) return R.error(res, "Unit not found.", 404);
|
||||
|
||||
const quiz = await UnitQuiz.findOne({
|
||||
where: { unit_id: unitId, ...onlyDeleted },
|
||||
paranoid: false,
|
||||
});
|
||||
if (!quiz) return R.error(res, "Archived quiz not found.", 404);
|
||||
return R.success(res, "Archived quiz retrieved.", { data: quiz.toJSON() });
|
||||
} catch (err) {
|
||||
console.error("[UNIT LIB][QUIZ][GET ARCHIVE]", err);
|
||||
return R.error(res, "Could not retrieve archived quiz.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.restoreQuiz = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { unitId, quizId } = req.params;
|
||||
const record = await restoreOne(UnitQuiz, { quiz_id: quizId, unit_id: unitId, ...onlyDeleted }, req.user?.user_id, t);
|
||||
if (!record) return R.error(res, "Archived quiz not found.", 404);
|
||||
await t.commit();
|
||||
logActivity(req.user?.user_id, "restore_quiz", { entityType: "quiz", entityId: Number(quizId) });
|
||||
return R.success(res, "Quiz restored.", { data: record });
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[UNIT LIB][QUIZ][RESTORE]", err);
|
||||
return R.error(res, "Could not restore quiz.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user