mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user