Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-11 12:12:29 +08:00
parent e1ffdab190
commit 82ea9c77c4
19 changed files with 530 additions and 122 deletions
+116 -4
View File
@@ -26,13 +26,14 @@ const { permanentDeleteOne, permanentDeleteMany } = require("../../utils/courses
const { getFieldValues } = require("../../utils/fieldValues.util");
const { flattenLessons, nextOrderIndex, reorderJunction } = require("../../utils/courses/hierarchy.util");
const { recomputeUnitDuration, recomputeCourseDuration } = require("../../utils/duration.util");
const { syncObjectivesCreate } = require("../../utils/courses/objectives.util");
const logActivity = require("../../utils/logActivity.util");
// ── Models ────────────────────────────────────────────────────────────────────
const {
Course, Unit, Lesson,
CourseUnit, UnitLesson,
Course, Unit, Lesson, LessonPage,
CourseUnit, UnitLesson, LessonObjective,
UnitQuiz, QuizQuestion, QuizOption,
UnitReadingProgress, LessonReadingProgress,
} = require("../../models/courses/courses.associations");
@@ -79,6 +80,27 @@ const UNIT_LIST_COMPUTED = [
WHERE cu.unit_id = "Unit"."unit_id"
)`,
},
{
key: "course_status",
label: "Course Status",
type: "text",
filterable: false,
literal: `(
CASE
WHEN NOT EXISTS (
SELECT 1 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"
) THEN 'standalone'
WHEN EXISTS (
SELECT 1 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" AND c.status = 'published'
) THEN 'published'
ELSE 'draft'
END
)`,
},
];
// ══════════════════════════════════════════════════════════════════════════════
@@ -103,7 +125,7 @@ exports.getUnits = async (req, res) => {
}
};
// Lightweight list for attach pickers: { unit_id, uuid, title, lesson_count, course_count }
// Lightweight list for attach pickers: { unit_id, uuid, title, lesson_count, course_count, course_title }
exports.getUnitsFlat = async (req, res) => {
try {
const rows = await sequelize.query(`
@@ -114,7 +136,11 @@ exports.getUnitsFlat = async (req, res) => {
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
WHERE cu.unit_id = u.unit_id) AS course_count,
(SELECT c.title 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
LIMIT 1) AS course_title
FROM units u
WHERE u."deletedAt" IS NULL
ORDER BY u.title ASC
@@ -190,6 +216,92 @@ exports.createUnit = async (req, res) => {
}
};
// One consolidated call: creates the unit, its lessons (with objectives + page
// blocks), and attaches each lesson to the unit — all in a single transaction.
// Body: { title, description, subscription,
// lessons: [{ title, description, objectives?: string[], blocks?: [] }],
// createdBy }
const CREATE_UNIT_FULL_LIMITS = { maxLessons: 50, maxObjectives: 20, maxBlocks: 100 };
exports.createUnitFull = async (req, res) => {
const t = await sequelize.transaction();
try {
const { title, description, subscription, lessons = [], createdBy } = req.body;
if (!title) return R.error(res, "Title is required.", 400);
if (!Array.isArray(lessons)) return R.error(res, "Lessons must be a list.", 400);
if (lessons.length > CREATE_UNIT_FULL_LIMITS.maxLessons) {
return R.error(res, `A unit can have at most ${CREATE_UNIT_FULL_LIMITS.maxLessons} lessons.`, 400);
}
for (const lessonInput of lessons) {
if (!lessonInput?.title) return R.error(res, "Each lesson needs a title.", 400);
if (lessonInput.objectives !== undefined && !Array.isArray(lessonInput.objectives)) {
return R.error(res, "Lesson objectives must be a list.", 400);
}
if (lessonInput.objectives?.length > CREATE_UNIT_FULL_LIMITS.maxObjectives) {
return R.error(res, `A lesson can have at most ${CREATE_UNIT_FULL_LIMITS.maxObjectives} objectives.`, 400);
}
if (lessonInput.blocks !== undefined && !Array.isArray(lessonInput.blocks)) {
return R.error(res, "Lesson page blocks must be a list.", 400);
}
if (lessonInput.blocks?.length > CREATE_UNIT_FULL_LIMITS.maxBlocks) {
return R.error(res, `A lesson page can have at most ${CREATE_UNIT_FULL_LIMITS.maxBlocks} blocks.`, 400);
}
}
const by = createdBy ?? req.user?.user_id ?? null;
const unit = await Unit.create({
title,
subscription: subscription || null,
description: description ?? null,
duration_seconds: 0,
createdBy: by,
}, { transaction: t });
for (let i = 0; i < lessons.length; i++) {
const lessonInput = lessons[i];
const lesson = await Lesson.create({
title: lessonInput.title,
description: lessonInput.description ?? null,
duration_seconds: 0,
createdBy: by,
}, { transaction: t });
await LessonPage.create({
lesson_id: lesson.lesson_id,
blocks: lessonInput.blocks ?? [],
createdBy: by,
}, { transaction: t });
await syncObjectivesCreate(LessonObjective, "lesson_id", lesson.lesson_id, lessonInput.objectives ?? [], t);
await UnitLesson.create({
unit_id: unit.unit_id,
lesson_id: lesson.lesson_id,
order_index: i,
createdBy: by,
}, { transaction: t });
}
await t.commit();
if (lessons.length) {
try { await recomputeUnitDuration(unit.unit_id); }
catch (durErr) { console.error("[UNIT LIB][CREATE FULL][DURATION]", durErr); }
}
logActivity(req.user?.user_id, "create_unit", { entityType: "unit", entityId: unit.unit_id, details: { title: unit.title, lessons: lessons.length } });
return R.success(res, "Unit created.", { data: unit }, 201);
} catch (err) {
await t.rollback();
console.error("[UNIT LIB][CREATE FULL]", err);
return R.error(res, "Could not create unit.", 500);
}
};
exports.updateUnit = async (req, res) => {
try {
const { unitId } = req.params;