units,lesson as standalone

This commit is contained in:
2026-07-10 11:44:46 +08:00
parent 86fba50b95
commit e1ffdab190
46 changed files with 1463 additions and 197 deletions
+12 -3
View File
@@ -41,6 +41,8 @@ const Task = sequelize.define('Task', {
name: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: true }, order: 1, filterable: true },
description: { type: DataTypes.TEXT, allowNull: true, hidden: true, filterable: false },
deadline: { type: DataTypes.DATE, allowNull: true, order: 2, filterable: true },
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, order: 2.1, filterable: true, comment: 'Position within the task list — drives sequencing lock.' },
is_required: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, order: 2.2, filterable: true, comment: 'Optional tasks do not block later tasks in the sequencing lock.' },
status: { type: DataTypes.ENUM('pending', 'in_progress', 'completed', 'overdue'), defaultValue: 'pending', allowNull: false, filterable: true },
// ── Audit trails ────────────────────────────────────────────────────────
@@ -56,7 +58,7 @@ const Task = sequelize.define('Task', {
const TaskRequirement = sequelize.define('TaskRequirement', {
requirement_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
task_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' } },
type: { type: DataTypes.ENUM('visit_link', 'upload_file', 'read_course', 'read_unit', 'read_lesson'), allowNull: false, filterable: true },
type: { type: DataTypes.ENUM('visit_link', 'upload_file', 'read_course', 'read_unit', 'read_lesson', 'submit_text', 'pass_quiz'), allowNull: false, filterable: true },
// ── visit_link ──────────────────────────────────────────────────────────
link_url: { type: DataTypes.STRING, allowNull: true, filterable: false },
@@ -66,9 +68,16 @@ const TaskRequirement = sequelize.define('TaskRequirement', {
allowed_file_types: { type: DataTypes.JSONB, allowNull: true, filterable: false },
max_file_count: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 1, filterable: false },
// ── read_course / read_unit / read_lesson ────────────────────────────────
reference_id: { type: DataTypes.UUID, allowNull: true, comment: 'course_id | unit_id | lesson_id depending on type', filterable: false },
// ── read_course / read_unit / read_lesson / pass_quiz ─────────────────────
reference_id: { type: DataTypes.UUID, allowNull: true, comment: 'course_id | unit_id | lesson_id | quiz_id depending on type', filterable: false },
reference_label: { type: DataTypes.STRING, allowNull: true, comment: 'Cached display name so we do not always join', filterable: false },
// ── submit_text ─────────────────────────────────────────────────────────
prompt: { type: DataTypes.TEXT, allowNull: true, comment: 'Instructions shown above the free-text response box.', filterable: false },
// ── upload_file / submit_text ──────────────────────────────────────────
requires_review: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, filterable: true, comment: 'If true, a submission only counts as complete once an admin approves it.' },
order: { type: DataTypes.INTEGER, defaultValue: 0, filterable: true },
// ── Audit trails ────────────────────────────────────────────────────────
+19 -1
View File
@@ -42,13 +42,31 @@ const TaskCompletion = sequelize.define('TaskCompletion', {
comment: 'Optional note the user attaches when submitting.',
order: 1,
},
response_text: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'The learner\'s free-text answer, for submit_text requirements.',
order: 1.5,
},
submitted_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
allowNull: false,
order: 2,
},
// ── Review workflow (requires_review requirements only) ──────────────────
status: {
type: DataTypes.ENUM('submitted', 'approved', 'rejected'),
allowNull: false,
defaultValue: 'submitted',
order: 2.1,
filterable: true,
},
reviewed_by: { type: DataTypes.BIGINT, allowNull: true, order: 2.2 },
reviewed_at: { type: DataTypes.DATE, allowNull: true, order: 2.3 },
review_note: { type: DataTypes.TEXT, allowNull: true, comment: 'Optional note the admin leaves when approving/rejecting.', order: 2.4 },
// ── Audit trails ────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true, order: 6 },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, order: 7 },
+2 -2
View File
@@ -99,10 +99,10 @@ const TaskProgress = sequelize.define('TaskProgress', {
reference_id: {
type: DataTypes.UUID,
allowNull: false,
comment: 'course_id | unit_id | lesson_id depending on type.',
comment: 'course_id | unit_id | lesson_id | quiz_id depending on type.',
},
type: {
type: DataTypes.ENUM('read_course', 'read_unit', 'read_lesson'),
type: DataTypes.ENUM('read_course', 'read_unit', 'read_lesson', 'pass_quiz'),
allowNull: false,
},
completed: {