|
|
|
@@ -16,6 +16,8 @@ const { getFieldValues } = require("../../utils/fieldValues.util");
|
|
|
|
|
const logActivity = require('../../utils/logActivity.util');
|
|
|
|
|
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
|
|
|
|
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
|
|
|
|
const CompletionRequirement = require('../../models/courses/completion_requirement.mdl');
|
|
|
|
|
const { VALID_ENTITY_TYPES } = require('../../utils/courses/completion_requirements.registry');
|
|
|
|
|
|
|
|
|
|
// ── Models ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
@@ -218,6 +220,23 @@ exports.createCourseFull = async (req, res) => {
|
|
|
|
|
await t.rollback();
|
|
|
|
|
return R.error(res, "Each new unit needs a title.", 400);
|
|
|
|
|
}
|
|
|
|
|
// Requirements only apply to units being created fresh here — units
|
|
|
|
|
// attached by unit_id already have their own configured set.
|
|
|
|
|
if (!unit.unit_id && unit.requirements?.length) {
|
|
|
|
|
const seenTypes = new Set();
|
|
|
|
|
for (const r of unit.requirements) {
|
|
|
|
|
const allowedEntityTypes = VALID_ENTITY_TYPES[r.type];
|
|
|
|
|
if (!allowedEntityTypes || !allowedEntityTypes.includes("unit")) {
|
|
|
|
|
await t.rollback();
|
|
|
|
|
return R.error(res, `"${r.type}" cannot be configured on a unit.`, 400);
|
|
|
|
|
}
|
|
|
|
|
if (seenTypes.has(r.type)) {
|
|
|
|
|
await t.rollback();
|
|
|
|
|
return R.error(res, `Duplicate "${r.type}" requirement — only one per unit is allowed.`, 400);
|
|
|
|
|
}
|
|
|
|
|
seenTypes.add(r.type);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const lesson of unit.lessons ?? []) {
|
|
|
|
|
if (!lesson.lesson_id && !lesson.title) {
|
|
|
|
|
await t.rollback();
|
|
|
|
@@ -301,6 +320,21 @@ exports.createCourseFull = async (req, res) => {
|
|
|
|
|
duration_seconds: 0,
|
|
|
|
|
createdBy: by,
|
|
|
|
|
}, { transaction: t });
|
|
|
|
|
|
|
|
|
|
if (unitInput.requirements?.length) {
|
|
|
|
|
const rows = unitInput.requirements.map((r, ri) => ({
|
|
|
|
|
entity_type: "unit",
|
|
|
|
|
entity_id: unit.unit_id,
|
|
|
|
|
type: r.type,
|
|
|
|
|
order: r.order ?? ri,
|
|
|
|
|
min_percent: r.type === "watch_percent" ? (r.min_percent ?? 100) : null,
|
|
|
|
|
button_label: r.type === "manual_complete" ? (r.button_label || null) : null,
|
|
|
|
|
is_required: r.is_required ?? true,
|
|
|
|
|
createdBy: by,
|
|
|
|
|
updatedBy: by,
|
|
|
|
|
}));
|
|
|
|
|
await CompletionRequirement.bulkCreate(rows, { transaction: t });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await CourseUnit.create({
|
|
|
|
@@ -658,10 +692,15 @@ exports.syncPrerequisites = async (req, res) => {
|
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
// Literal fragments that resolve a unit's order within THIS course.
|
|
|
|
|
// course_id is a 19-digit snowflake-style BIGINT — well past
|
|
|
|
|
// Number.MAX_SAFE_INTEGER, so Number(courseId) silently rounds to the wrong
|
|
|
|
|
// id and these subqueries match nothing. BigInt preserves full precision
|
|
|
|
|
// (and throws on non-numeric input, same effect as the old NaN would've had
|
|
|
|
|
// via the caller's try/catch).
|
|
|
|
|
const courseOrderLiteral = (courseId) =>
|
|
|
|
|
`(SELECT cu.order_index FROM course_units cu WHERE cu.course_id = ${Number(courseId)} AND cu.unit_id = "Unit"."unit_id" LIMIT 1)`;
|
|
|
|
|
`(SELECT cu.order_index FROM course_units cu WHERE cu.course_id = ${BigInt(courseId)} AND cu.unit_id = "Unit"."unit_id" LIMIT 1)`;
|
|
|
|
|
const courseMembershipLiteral = (courseId) =>
|
|
|
|
|
`(SELECT cu.unit_id FROM course_units cu WHERE cu.course_id = ${Number(courseId)})`;
|
|
|
|
|
`(SELECT cu.unit_id FROM course_units cu WHERE cu.course_id = ${BigInt(courseId)})`;
|
|
|
|
|
|
|
|
|
|
exports.getUnits = async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
@@ -1197,10 +1236,12 @@ exports.getUnitPermanentDeleteImpact = async (req, res) => {
|
|
|
|
|
// Entity-level archive/permanent-delete lives in /admin/lessons (the library).
|
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
// Same BigInt fix as courseOrderLiteral/courseMembershipLiteral above —
|
|
|
|
|
// unit_id is a 19-digit snowflake BIGINT, past Number.MAX_SAFE_INTEGER.
|
|
|
|
|
const unitOrderLiteral = (unitId) =>
|
|
|
|
|
`(SELECT ul.order_index FROM unit_lessons ul WHERE ul.unit_id = ${Number(unitId)} AND ul.lesson_id = "Lesson"."lesson_id" LIMIT 1)`;
|
|
|
|
|
`(SELECT ul.order_index FROM unit_lessons ul WHERE ul.unit_id = ${BigInt(unitId)} AND ul.lesson_id = "Lesson"."lesson_id" LIMIT 1)`;
|
|
|
|
|
const unitMembershipLiteral = (unitId) =>
|
|
|
|
|
`(SELECT ul.lesson_id FROM unit_lessons ul WHERE ul.unit_id = ${Number(unitId)})`;
|
|
|
|
|
`(SELECT ul.lesson_id FROM unit_lessons ul WHERE ul.unit_id = ${BigInt(unitId)})`;
|
|
|
|
|
|
|
|
|
|
exports.getLessons = async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|