"use strict"; 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"); // ── Models ──────────────────────────────────────────────────────────────────── const { Course, Unit, Lesson, UnitQuiz, QuizQuestion, QuizOption, } = require("../../models/courses/courses.associations"); const mdl_Users = require("../../models/users/users.mdl"); const notDeleted = { deletedAt: null }; const onlyDeleted = { deletedAt: { [Op.not]: null } }; // ══════════════════════════════════════════════════════════════════════════════ // UNIT // ══════════════════════════════════════════════════════════════════════════════ 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", findOptions: { where: { course_id: courseId, ...notDeleted }, order: [["order_index", "ASC"]], }, 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); return R.error(res, "Could not retrieve units.", 500); } }; exports.getUnit = async (req, res) => { try { const { courseId, unitId } = req.params; const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted }, include: [ { model: Lesson, as: "lessons", where: notDeleted, required: false }, { model: UnitQuiz, as: "quiz", required: false }, ], 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() }); } catch (err) { console.error("[UNIT][GET ONE]", err); return R.error(res, "Could not retrieve unit.", 500); } }; exports.createUnit = async (req, res) => { try { const { courseId } = req.params; const { title, description, 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, }); return R.success(res, "Unit created.", { data: unit }, 201); } catch (err) { console.error("[UNIT][CREATE]", err); return R.error(res, "Could not create unit.", 500); } }; exports.updateUnit = 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 { title, description, order, 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; await unit.save(); return R.success(res, "Unit updated.", { data: unit }); } catch (err) { console.error("[UNIT][UPDATE]", err); return R.error(res, "Could not update unit.", 500); } }; exports.deleteUnit = 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 ); if (!record) return R.error(res, "Unit not found.", 404); await t.commit(); return R.success(res, "Unit archived."); } catch (err) { await t.rollback(); console.error("[UNIT][DELETE]", err); return R.error(res, "Could not archive unit.", 500); } }; exports.bulkArchiveUnits = async (req, res) => { const t = await sequelize.transaction(); try { const { courseId } = req.params; const { ids = [], deletedBy } = 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 validIds = units.map((u) => u.unit_id); const count = await archiveMany(Unit, "unit_id", validIds, deletedBy, t); await t.commit(); return R.success(res, `${count} unit${count !== 1 ? "s" : ""} archived.`); } catch (err) { await t.rollback(); console.error("[UNIT][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", findOptions: { where: { course_id: courseId, ...onlyDeleted }, paranoid: false, order: [["order_index", "ASC"]], }, }); return R.success(res, "Archived units retrieved.", result); } catch (err) { console.error("[UNIT][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 unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...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); return R.error(res, "Could not retrieve archived unit.", 500); } }; 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 ); if (!record) return R.error(res, "Archived unit not found.", 404); await t.commit(); return R.success(res, "Unit restored.", { data: record }); } catch (err) { await t.rollback(); console.error("[UNIT][RESTORE]", err); return R.error(res, "Could not restore unit.", 500); } }; exports.bulkRestoreUnits = async (req, res) => { const t = await sequelize.transaction(); try { const { courseId } = req.params; const { ids = [], restoredBy } = 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 validIds = units.map((u) => u.unit_id); const count = await restoreMany(Unit, "unit_id", validIds, restoredBy, t); await t.commit(); return R.success(res, `${count} unit${count !== 1 ? "s" : ""} restored.`); } catch (err) { await t.rollback(); console.error("[UNIT][BULK RESTORE]", err); return R.error(res, "Could not restore units.", 500); } }; // ══════════════════════════════════════════════════════════════════════════════ // UNIT QUIZ // ══════════════════════════════════════════════════════════════════════════════ exports.getQuiz = 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 quiz = await UnitQuiz.findOne({ where: { unit_id: unitId, ...notDeleted }, include: [{ model: QuizQuestion, as: "questions", where: notDeleted, required: false, include: [{ model: QuizOption, as: "options", order: [["order_index", "ASC"]] }], }], }); 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); 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 unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...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, }); return R.success(res, "Quiz created.", { data: quiz }, 201); } catch (err) { console.error("[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 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; await quiz.save(); return R.success(res, "Quiz updated.", { data: quiz }); } catch (err) { console.error("[QUIZ][UPDATE]", err); return R.error(res, "Could not update quiz.", 500); } }; 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 ); if (!record) return R.error(res, "Quiz not found.", 404); await t.commit(); return R.success(res, "Quiz archived."); } catch (err) { await t.rollback(); console.error("[QUIZ][DELETE]", err); return R.error(res, "Could not archive quiz.", 500); } };