mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -24,10 +24,13 @@ const {
|
||||
UnitQuiz, QuizQuestion, QuizOption,
|
||||
QuizAttempt, AssessmentSession,
|
||||
CourseInstructor, CourseAchievement,
|
||||
UnitReadingProgress, LessonReadingProgress,
|
||||
} = require("../../models/courses/courses.associations");
|
||||
|
||||
const mdl_Users = require("../../models/users/users.mdl");
|
||||
|
||||
const { mdl_PlanCourses, mdl_TierPlans } = require("../../models/tiers/tier.associations");
|
||||
|
||||
const {
|
||||
excludeAttributes: courseExclude,
|
||||
computedAttributes: courseComputed,
|
||||
@@ -46,7 +49,15 @@ exports.getCourses = async (req, res) => {
|
||||
try {
|
||||
const result = await paginate(Course, req, {
|
||||
excludeAttributes: courseExclude,
|
||||
computedAttributes: courseComputed,
|
||||
computedAttributes: [
|
||||
...courseComputed,
|
||||
{
|
||||
key: "assessment_id",
|
||||
label: "Assessment ID",
|
||||
type: "text",
|
||||
literal: `(SELECT assessment_id FROM course_assessments WHERE course_id = "Course"."course_id" AND "deletedAt" IS NULL LIMIT 1)`,
|
||||
},
|
||||
],
|
||||
auditOptions: { mdl_Users, parentAlias: "Course" },
|
||||
context: "list",
|
||||
findOptions: { where: { ...notDeleted } },
|
||||
@@ -126,7 +137,7 @@ exports.createCourse = async (req, res) => {
|
||||
|
||||
if (achievement_keys.length) {
|
||||
await CourseAchievement.bulkCreate(
|
||||
achievement_keys.slice(0, 3).map((key, i) => ({ course_id: course.course_id, achievement_key: key, order_index: i })),
|
||||
achievement_keys.slice(0, 1).map((key, i) => ({ course_id: course.course_id, achievement_key: key, order_index: i })),
|
||||
{ transaction: t },
|
||||
);
|
||||
}
|
||||
@@ -278,6 +289,30 @@ exports.bulkRestoreCourses = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.getCourseArchiveImpact = async (req, res) => {
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
|
||||
const [activeCount, totalCount] = await Promise.all([
|
||||
UnitReadingProgress.count({
|
||||
where: { course_id: courseId, status: "in_progress" },
|
||||
distinct: true,
|
||||
col: "user_id",
|
||||
}),
|
||||
UnitReadingProgress.count({
|
||||
where: { course_id: courseId },
|
||||
distinct: true,
|
||||
col: "user_id",
|
||||
}),
|
||||
]);
|
||||
|
||||
return R.success(res, "Impact retrieved.", { activeCount, totalCount });
|
||||
} catch (err) {
|
||||
console.error("[COURSE][ARCHIVE IMPACT]", err);
|
||||
return R.error(res, "Could not retrieve impact.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE PREREQUISITES
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -364,6 +399,22 @@ exports.getUnits = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUnitArchiveImpact = async (req, res) => {
|
||||
try {
|
||||
const { unitId } = req.params;
|
||||
|
||||
const [completionCount, progressCount] = await Promise.all([
|
||||
UnitReadingProgress.count({ where: { unit_id: unitId, status: "completed" } }),
|
||||
LessonReadingProgress.count({ where: { unit_id: unitId }, distinct: true, col: "user_id" }),
|
||||
]);
|
||||
|
||||
return R.success(res, "Impact retrieved.", { completionCount, progressCount });
|
||||
} catch (err) {
|
||||
console.error("[UNIT][ARCHIVE IMPACT]", err);
|
||||
return R.error(res, "Could not retrieve impact.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUnit = async (req, res) => {
|
||||
try {
|
||||
const { courseId, unitId } = req.params;
|
||||
@@ -1234,6 +1285,87 @@ exports.bulkRestoreQuestions = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.bulkSyncQuestions = 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 { questions = [], updatedBy } = req.body;
|
||||
|
||||
const existing = await QuizQuestion.findAll({
|
||||
where: { [parent.parentField]: parent.parentId, ...notDeleted },
|
||||
});
|
||||
const existingIds = existing.map((q) => q.question_id);
|
||||
const incomingIds = questions.filter((q) => q.question_id).map((q) => q.question_id);
|
||||
const toArchive = existingIds.filter((id) => !incomingIds.includes(id));
|
||||
|
||||
if (toArchive.length) {
|
||||
await QuizQuestion.update(
|
||||
{ deletedAt: new Date(), deletedBy: updatedBy ?? null },
|
||||
{ where: { question_id: toArchive }, transaction: t }
|
||||
);
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const { question_id, type, question, explanation, points, options = [] } = questions[i];
|
||||
|
||||
if (question_id && existingIds.includes(question_id)) {
|
||||
const q = existing.find((e) => e.question_id === question_id);
|
||||
q.type = type ?? q.type;
|
||||
q.question = question ?? q.question;
|
||||
q.explanation = explanation ?? null;
|
||||
q.order_index = i;
|
||||
q.points = points ?? q.points;
|
||||
q.updatedBy = updatedBy ?? null;
|
||||
await q.save({ transaction: t });
|
||||
|
||||
await QuizOption.destroy({ where: { question_id }, transaction: t });
|
||||
if (options.length) {
|
||||
await QuizOption.bulkCreate(
|
||||
options.map((o, oi) => ({ question_id, text: o.text, is_correct: o.is_correct ?? false, order_index: oi })),
|
||||
{ transaction: t }
|
||||
);
|
||||
}
|
||||
result.push(question_id);
|
||||
} else {
|
||||
const q = await QuizQuestion.create({
|
||||
[parent.parentField]: parent.parentId,
|
||||
type, question,
|
||||
explanation: explanation ?? null,
|
||||
order_index: i,
|
||||
points: points ?? 1,
|
||||
createdBy: updatedBy ?? null,
|
||||
}, { transaction: t });
|
||||
|
||||
if (options.length) {
|
||||
await QuizOption.bulkCreate(
|
||||
options.map((o, oi) => ({ question_id: q.question_id, text: o.text, is_correct: o.is_correct ?? false, order_index: oi })),
|
||||
{ transaction: t }
|
||||
);
|
||||
}
|
||||
result.push(q.question_id);
|
||||
}
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
|
||||
const synced = await QuizQuestion.findAll({
|
||||
where: { question_id: result },
|
||||
order: [["order_index", "ASC"]],
|
||||
include: [{ model: QuizOption, as: "options", order: [["order_index", "ASC"]] }],
|
||||
});
|
||||
|
||||
logActivity(req.user?.user_id, 'bulk_sync_questions', { entityType: 'question', details: { parentField: parent.parentField, parentId: parent.parentId, count: synced.length } });
|
||||
return R.success(res, "Questions synced.", { data: synced });
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[QUESTION][BULK SYNC]", err);
|
||||
return R.error(res, "Could not sync questions.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE ASSESSMENT
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1341,11 +1473,12 @@ exports.updateAssessment = async (req, res) => {
|
||||
|
||||
const course = await Course.findOne({
|
||||
where: { course_id: courseId },
|
||||
attributes: ['title'],
|
||||
attributes: ['title', 'uuid'],
|
||||
});
|
||||
const notify = NOTIFICATION_REGISTRY.assessment_updated.build({
|
||||
assessmentTitle: assessment.title,
|
||||
courseTitle: course?.title ?? null,
|
||||
courseUuid: course?.uuid ?? null,
|
||||
});
|
||||
const now = new Date();
|
||||
await UserNotification.bulkCreate(
|
||||
@@ -1433,7 +1566,7 @@ exports.getCoursesFlat = async (req, res) => {
|
||||
try {
|
||||
const data = await Course.findAll({
|
||||
where: notDeleted,
|
||||
attributes: ["uuid", "title"],
|
||||
attributes: ["uuid", "title", "subscription", "duration_seconds"],
|
||||
order: [["title", "ASC"]],
|
||||
});
|
||||
return R.success(res, "Courses retrieved.", data);
|
||||
@@ -1448,11 +1581,32 @@ exports.getCoursesBySubscription = async (req, res) => {
|
||||
const { slug } = req.query;
|
||||
if (!slug) return R.error(res, 'slug query param is required.', 400);
|
||||
|
||||
const data = await Course.findAll({
|
||||
const rows = await Course.findAll({
|
||||
where: { ...notDeleted, subscription: slug },
|
||||
attributes: ['course_id', 'title', 'description', 'subscription'],
|
||||
include: [{
|
||||
model: mdl_PlanCourses,
|
||||
as: 'planCourse',
|
||||
required: false,
|
||||
attributes: ['plan_id'],
|
||||
include: [{
|
||||
model: mdl_TierPlans,
|
||||
as: 'plan',
|
||||
attributes: ['plan_id', 'label'],
|
||||
}],
|
||||
}],
|
||||
order: [['title', 'ASC']],
|
||||
});
|
||||
|
||||
// Flatten so the frontend can just check `assigned_plan` — a course belongs
|
||||
// to at most one plan (UNIQUE constraint on plan_courses.course_id).
|
||||
const data = rows.map((c) => {
|
||||
const plain = c.toJSON();
|
||||
const assigned_plan = plain.planCourse?.plan ?? null;
|
||||
delete plain.planCourse;
|
||||
return { ...plain, assigned_plan };
|
||||
});
|
||||
|
||||
return R.success(res, 'Courses retrieved.', data);
|
||||
} catch (err) {
|
||||
console.error('[COURSE][BY SUBSCRIPTION]', err);
|
||||
@@ -1464,11 +1618,12 @@ exports.getUnitsFlat = async (req, res) => {
|
||||
try {
|
||||
const rows = await Unit.findAll({
|
||||
where: notDeleted,
|
||||
attributes: ["uuid", "title", "order_index"],
|
||||
attributes: ["uuid", "title", "order_index", "duration_seconds"],
|
||||
include: [{
|
||||
model: Course,
|
||||
as: "course",
|
||||
attributes: ["title"],
|
||||
attributes: ["title", "subscription"],
|
||||
paranoid: false,
|
||||
}],
|
||||
order: [
|
||||
[{ model: Course, as: "course" }, "title", "ASC"],
|
||||
@@ -1476,10 +1631,12 @@ exports.getUnitsFlat = async (req, res) => {
|
||||
],
|
||||
});
|
||||
const data = rows.map((u) => ({
|
||||
uuid: u.uuid,
|
||||
title: u.title,
|
||||
order_index: u.order_index ?? 0,
|
||||
course_title: u.course?.title ?? "",
|
||||
uuid: u.uuid,
|
||||
title: u.title,
|
||||
order_index: u.order_index ?? 0,
|
||||
duration_seconds: u.duration_seconds ?? 0,
|
||||
course_title: u.course?.title ?? "",
|
||||
subscription: u.course?.subscription ?? "free",
|
||||
}));
|
||||
return R.success(res, "Units retrieved.", data);
|
||||
} catch (err) {
|
||||
@@ -1492,15 +1649,17 @@ exports.getLessonsFlat = async (req, res) => {
|
||||
try {
|
||||
const rows = await Lesson.findAll({
|
||||
where: notDeleted,
|
||||
attributes: ["uuid", "title", "order_index"],
|
||||
attributes: ["uuid", "title", "order_index", "duration_seconds"],
|
||||
include: [{
|
||||
model: Unit,
|
||||
as: "unit",
|
||||
attributes: ["title", "order_index"],
|
||||
paranoid: false,
|
||||
include: [{
|
||||
model: Course,
|
||||
as: "course",
|
||||
attributes: ["title"],
|
||||
attributes: ["title", "subscription"],
|
||||
paranoid: false,
|
||||
}],
|
||||
}],
|
||||
order: [
|
||||
@@ -1510,12 +1669,14 @@ exports.getLessonsFlat = async (req, res) => {
|
||||
],
|
||||
});
|
||||
const data = rows.map((l) => ({
|
||||
uuid: l.uuid,
|
||||
title: l.title,
|
||||
order_index: l.order_index ?? 0,
|
||||
unit_title: l.unit?.title ?? "",
|
||||
unit_order: l.unit?.order_index ?? 0,
|
||||
course_title: l.unit?.course?.title ?? "",
|
||||
uuid: l.uuid,
|
||||
title: l.title,
|
||||
order_index: l.order_index ?? 0,
|
||||
duration_seconds: l.duration_seconds ?? 0,
|
||||
unit_title: l.unit?.title ?? "",
|
||||
unit_order: l.unit?.order_index ?? 0,
|
||||
course_title: l.unit?.course?.title ?? "",
|
||||
subscription: l.unit?.course?.subscription ?? "free",
|
||||
}));
|
||||
return R.success(res, "Lessons retrieved.", data);
|
||||
} catch (err) {
|
||||
@@ -1801,8 +1962,8 @@ exports.syncCourseAchievements = async (req, res) => {
|
||||
const { courseId } = req.params;
|
||||
const { achievement_keys = [] } = req.body;
|
||||
|
||||
if (achievement_keys.length > 3)
|
||||
return R.error(res, "Maximum 3 achievements allowed per course.", 400);
|
||||
if (achievement_keys.length > 1)
|
||||
return R.error(res, "Maximum 1 achievement allowed per course.", 400);
|
||||
|
||||
await CourseAchievement.destroy({ where: { course_id: courseId }, transaction: t });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user