From 72a792d5853f908aa29946823ca167f327bfca7e Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Mon, 20 Jul 2026 08:17:30 +0800 Subject: [PATCH] ready --- controllers/admin/assets.controller.js | 13 ++++--------- controllers/admin/lessons.controller.js | 16 ++++++++++++++++ controllers/admin/units.controller.js | 4 ++++ models/courses/lessons.mdl.js | 16 ++++++++-------- utils/paginate.util.js | 5 +++-- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/controllers/admin/assets.controller.js b/controllers/admin/assets.controller.js index 23ad792..bcbcc3e 100644 --- a/controllers/admin/assets.controller.js +++ b/controllers/admin/assets.controller.js @@ -337,11 +337,11 @@ exports.getAsset = async (req, res) => { // └─────────────────────────────────────────────────────────────────────────┘ // // Shared by the single-file POST / and the multi-file POST /batch routes. -// `requireVideoThumbnail` is false for batch uploads — a bulk drop has no -// per-file thumbnail step, so videos land with thumbnail_url null and pick -// one up later via the existing "thumbnail-only" path in updateAsset(). +// Thumbnails are optional for both video and audio — a video/audio asset can +// land with thumbnail_url null and pick one up later via the existing +// "thumbnail-only" path in updateAsset(). // -async function createAssetFromUpload({ file, thumbFile, body, user, requireVideoThumbnail = true, uploadId }) { +async function createAssetFromUpload({ file, thumbFile, body, user, uploadId }) { const uploadedFiles = []; // [{ key, provider }] try { @@ -367,10 +367,6 @@ async function createAssetFromUpload({ file, thumbFile, body, user, requireVideo const checksum = file.buffer ? resolveChecksum(file.buffer) : null; const usesProvider = ["chibisafe", "s3"].includes(storage_provider); - if (file_type === "video" && requireVideoThumbnail && !thumbFile) { - throw Object.assign(new Error("A thumbnail image is required for video uploads."), { status: 400 }); - } - if (usesProvider && !file.buffer) { throw Object.assign(new Error("File buffer is required. Ensure multer uses memoryStorage."), { status: 400 }); } @@ -575,7 +571,6 @@ exports.uploadAssetsBatch = async (req, res) => { createdBy, }, user: req.user, - requireVideoThumbnail: false, }); results.push({ originalname: file.originalname, success: true, data: asset }); } catch (err) { diff --git a/controllers/admin/lessons.controller.js b/controllers/admin/lessons.controller.js index da3082c..7d48a41 100644 --- a/controllers/admin/lessons.controller.js +++ b/controllers/admin/lessons.controller.js @@ -55,6 +55,21 @@ async function recomputeParentDurations(lessonId) { } const LESSON_LIST_COMPUTED = [ + { + // Not its own column — consumed by the Title cell on the frontend to + // prefix "(UNIT)" when a lesson is already attached to at least one Unit. + key: "unit_count", + label: "Unit Count", + type: "number", + hidden: true, + filterable: false, + literal: `( + SELECT CAST(COUNT(*) AS INTEGER) + FROM unit_lessons ul + JOIN units u ON u.unit_id = ul.unit_id AND u."deletedAt" IS NULL + WHERE ul.lesson_id = "Lesson"."lesson_id" + )`, + }, { key: "course_count", label: "Affiliated", @@ -74,6 +89,7 @@ const LESSON_LIST_COMPUTED = [ label: "Course Status", type: "text", order: 3, // 1: Title, 2: Affiliated, 3: Course Status — see lessons.mdl.js + hidden: true, filterable: false, literal: `( CASE diff --git a/controllers/admin/units.controller.js b/controllers/admin/units.controller.js index 2cb50cb..7bcd096 100644 --- a/controllers/admin/units.controller.js +++ b/controllers/admin/units.controller.js @@ -72,10 +72,13 @@ const UNIT_LIST_COMPUTED = [ )`, }, { + // Not its own column — consumed by the Title cell on the frontend to + // show a "Course" badge when a unit is already attached to at least one Course. key: "course_count", label: "Affiliated", type: "number", order: 2, // 1: Title, 2: Affiliated, 3: Subscription, 4: Duration — see units.mdl.js + hidden: true, literal: `( SELECT CAST(COUNT(*) AS INTEGER) FROM course_units cu @@ -88,6 +91,7 @@ const UNIT_LIST_COMPUTED = [ label: "Course Status", type: "text", order: 3, // 1: Title, 2: Affiliated, 3: Course Status, 4: Subscription, 5: Duration — see units.mdl.js + hidden: true, filterable: false, literal: `( CASE diff --git a/models/courses/lessons.mdl.js b/models/courses/lessons.mdl.js index 2485bdf..bd546b2 100644 --- a/models/courses/lessons.mdl.js +++ b/models/courses/lessons.mdl.js @@ -4,18 +4,18 @@ const sequelize = require("../../config/db.config"); // Standalone entity — no unit_id / order_index here. A Lesson is attached to // zero or more Units through unit_lessons, where per-unit ordering lives. const Lesson = sequelize.define("Lesson", { - lesson_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, hidden: true, order: 0 }, - uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, hidden: true, order: 0 }, + lesson_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, hidden: true, order: 0, filterable: false }, + uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, hidden: true, order: 0, filterable: false }, - title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", hidden: false, order: 1 }, + title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", hidden: false, order: 1, filterable: true }, // order 2 is reserved for the computed "Affiliated" (course_count) column, order 3 for computed "Course Status" — see LESSON_LIST_COMPUTED in lessons.controller.js - description: { type: DataTypes.TEXT, allowNull: true, label: "Description", hidden: true, order: 0 }, + description: { type: DataTypes.TEXT, allowNull: true, label: "Description", hidden: true, order: 0, filterable: false }, - duration_seconds: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0 }, // computed from blocks on save + duration_seconds: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0, filterable: false }, // computed from blocks on save - createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" }, - updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By" }, - deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" }, + createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By", filterable: true }, + updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By", filterable: true }, + deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By", filterable: true }, }, { tableName: "lessons", timestamps: true, diff --git a/utils/paginate.util.js b/utils/paginate.util.js index 45fb1f3..1445888 100644 --- a/utils/paginate.util.js +++ b/utils/paginate.util.js @@ -174,13 +174,14 @@ async function paginate(model, req, { const totalPages = Math.ceil(count / limit); // ← Append computed metadata - const computedMeta = computedAttributes.map(({ key, label, type, order: ord, filterable }) => ({ + const computedMeta = computedAttributes.map(({ key, label, type, order: ord, filterable, hidden }) => ({ name: label ?? key, type: type ?? 'text', field: key, order: ord ?? Infinity, options: {}, - filterable: filterable + filterable: filterable, + hidden: hidden ?? false, })); // ← Merge, sort, THEN strip order