@hed-hog/lms 0.0.364 → 0.0.365
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +17 -9
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +17 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +12 -4
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +98 -23
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-hls.service.d.ts +57 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +767 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +45 -13
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +40 -26
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +11 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +6 -9
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +57 -48
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
- package/dist/course/dto/create-course-export.dto.d.ts +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +102 -8
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.js +32 -8
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +43 -2
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +11 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +59 -6
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +2 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +4 -2
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/training/training-student.service.d.ts +51 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/evaluation/evaluation.service.d.ts +18 -0
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +6 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +95 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +160 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +15 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
- package/dist/student-xp/student-xp.controller.js +24 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -1
- package/dist/student-xp/student-xp.service.d.ts +16 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -1
- package/dist/student-xp/student-xp.service.js +51 -1
- package/dist/student-xp/student-xp.service.js.map +1 -1
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +0 -17
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +94 -124
- package/hedhog/data/setting_group.yaml +19 -19
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
- package/hedhog/frontend/messages/en.json +26 -28
- package/hedhog/frontend/messages/pt.json +26 -28
- package/hedhog/table/course_export.yaml +62 -0
- package/package.json +13 -9
- package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-structure.controller.ts +14 -2
- package/src/course/course-structure.service.ts +100 -7
- package/src/course/course-video-hls.service.ts +946 -0
- package/src/course/course.controller.ts +33 -19
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +11 -0
- package/src/course/course.service.ts +73 -60
- package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/lms-bulk-upload-automation.service.ts +153 -6
- package/src/course/lms-bulk-upload-infra.service.ts +39 -6
- package/src/course/lms-bulk-upload.controller.ts +32 -2
- package/src/course/lms-bulk-upload.service.ts +70 -7
- package/src/course/lms-setting.controller.ts +4 -2
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +6 -5
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +95 -1
- package/src/platforma/platforma.service.ts +268 -268
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +18 -2
- package/src/student-xp/student-xp.service.ts +84 -2
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -138,7 +138,7 @@ export class TrainingStudentService {
|
|
|
138
138
|
orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
|
|
139
139
|
select: {
|
|
140
140
|
image_type: { select: { slug: true } },
|
|
141
|
-
file: { select: {
|
|
141
|
+
file: { select: { id: true } },
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
144
|
course_module: {
|
|
@@ -196,7 +196,8 @@ export class TrainingStudentService {
|
|
|
196
196
|
durationHours: row.duration_hours ?? 0,
|
|
197
197
|
level: row.level,
|
|
198
198
|
lessons,
|
|
199
|
-
image: banner?.file?.
|
|
199
|
+
image: banner?.file?.id ? `/file/open/${banner.file.id}` : logo?.file?.id ? `/file/open/${logo.file.id}` : null,
|
|
200
|
+
logoImage: logo?.file?.id ? `/file/open/${logo.file.id}` : null,
|
|
200
201
|
categoryKey: categorySlug,
|
|
201
202
|
categoryLabel: this.formatCategoryLabel(categorySlug),
|
|
202
203
|
enrollmentCount: row._count?.course_enrollment ?? 0,
|
|
@@ -214,6 +215,224 @@ export class TrainingStudentService {
|
|
|
214
215
|
};
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
async getPublishedCourseBySlug(slug: string, userId?: number) {
|
|
219
|
+
const row = await this.prisma.course.findFirst({
|
|
220
|
+
where: { slug, status: 'published', is_listed: true },
|
|
221
|
+
select: {
|
|
222
|
+
id: true,
|
|
223
|
+
slug: true,
|
|
224
|
+
title: true,
|
|
225
|
+
description: true,
|
|
226
|
+
duration_hours: true,
|
|
227
|
+
level: true,
|
|
228
|
+
primary_color: true,
|
|
229
|
+
course_category: {
|
|
230
|
+
select: { category: { select: { slug: true } } },
|
|
231
|
+
},
|
|
232
|
+
course_image: {
|
|
233
|
+
where: { image_type: { slug: { in: ['course-banner', 'course-logo'] } } },
|
|
234
|
+
orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
|
|
235
|
+
select: {
|
|
236
|
+
image_type: { select: { slug: true } },
|
|
237
|
+
file: { select: { id: true } },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
course_module: {
|
|
241
|
+
orderBy: { order: 'asc' },
|
|
242
|
+
select: {
|
|
243
|
+
id: true,
|
|
244
|
+
title: true,
|
|
245
|
+
duration_minutes: true,
|
|
246
|
+
course_lesson: {
|
|
247
|
+
where: { published: true },
|
|
248
|
+
orderBy: { order: 'asc' },
|
|
249
|
+
select: {
|
|
250
|
+
id: true,
|
|
251
|
+
title: true,
|
|
252
|
+
type: true,
|
|
253
|
+
duration_seconds: true,
|
|
254
|
+
course_lesson_file: {
|
|
255
|
+
where: {
|
|
256
|
+
is_public: true,
|
|
257
|
+
file_id: { not: null },
|
|
258
|
+
OR: [{ type: null }, { type: { not: { startsWith: 'video' } } }],
|
|
259
|
+
},
|
|
260
|
+
select: { id: true },
|
|
261
|
+
take: 1,
|
|
262
|
+
},
|
|
263
|
+
course_lesson_instructor: {
|
|
264
|
+
take: 1,
|
|
265
|
+
select: {
|
|
266
|
+
instructor: {
|
|
267
|
+
select: {
|
|
268
|
+
person: {
|
|
269
|
+
select: {
|
|
270
|
+
name: true,
|
|
271
|
+
file: { select: { id: true } },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!row) throw new NotFoundException(`Course "${slug}" not found`);
|
|
286
|
+
|
|
287
|
+
const banner = row.course_image.find((img) => img.image_type?.slug === 'course-banner');
|
|
288
|
+
const logo = row.course_image.find((img) => img.image_type?.slug === 'course-logo');
|
|
289
|
+
const categorySlug = row.course_category[0]?.category?.slug ?? null;
|
|
290
|
+
|
|
291
|
+
const firstInstructor = row.course_module
|
|
292
|
+
.flatMap((m) => m.course_lesson)
|
|
293
|
+
.flatMap((l) => l.course_lesson_instructor)
|
|
294
|
+
.find((cli) => cli.instructor?.person);
|
|
295
|
+
|
|
296
|
+
const instructor = firstInstructor?.instructor?.person
|
|
297
|
+
? {
|
|
298
|
+
name: firstInstructor.instructor.person.name,
|
|
299
|
+
avatar: firstInstructor.instructor.person.file?.id ? `/file/open/${firstInstructor.instructor.person.file.id}` : null,
|
|
300
|
+
}
|
|
301
|
+
: null;
|
|
302
|
+
|
|
303
|
+
let completedLessonIds: number[] = [];
|
|
304
|
+
let progressPercent = 0;
|
|
305
|
+
|
|
306
|
+
if (userId) {
|
|
307
|
+
const personUser = await this.prisma.person_user.findFirst({
|
|
308
|
+
where: { user_id: userId },
|
|
309
|
+
select: { person_id: true },
|
|
310
|
+
});
|
|
311
|
+
if (personUser) {
|
|
312
|
+
const enrollment = await this.prisma.course_enrollment.findFirst({
|
|
313
|
+
where: { person_id: personUser.person_id, course_id: row.id, status: { not: 'cancelled' } },
|
|
314
|
+
select: { id: true, progress_percent: true },
|
|
315
|
+
});
|
|
316
|
+
if (enrollment) {
|
|
317
|
+
progressPercent = enrollment.progress_percent ?? 0;
|
|
318
|
+
const completedRows = await this.prisma.course_lesson_progress.findMany({
|
|
319
|
+
where: { course_enrollment_id: enrollment.id, status: 'completed' },
|
|
320
|
+
select: { course_lesson_id: true },
|
|
321
|
+
});
|
|
322
|
+
completedLessonIds = completedRows.map((r) => r.course_lesson_id);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
id: row.id,
|
|
329
|
+
slug: row.slug,
|
|
330
|
+
title: row.title,
|
|
331
|
+
description: row.description ?? '',
|
|
332
|
+
durationHours: row.duration_hours ?? 0,
|
|
333
|
+
level: row.level,
|
|
334
|
+
image: banner?.file?.id ? `/file/open/${banner.file.id}` : logo?.file?.id ? `/file/open/${logo.file.id}` : null,
|
|
335
|
+
logoImage: logo?.file?.id ? `/file/open/${logo.file.id}` : null,
|
|
336
|
+
primaryColor: row.primary_color ?? null,
|
|
337
|
+
categoryKey: categorySlug,
|
|
338
|
+
categoryLabel: this.formatCategoryLabel(categorySlug),
|
|
339
|
+
completedLessonIds,
|
|
340
|
+
progressPercent,
|
|
341
|
+
modules: row.course_module.map((m) => ({
|
|
342
|
+
id: m.id,
|
|
343
|
+
title: m.title,
|
|
344
|
+
lessonCount: m.course_lesson.length,
|
|
345
|
+
durationHours: (m.duration_minutes ?? 0) / 60,
|
|
346
|
+
lessons: m.course_lesson.map((l) => ({
|
|
347
|
+
id: l.id,
|
|
348
|
+
title: l.title,
|
|
349
|
+
type: l.type,
|
|
350
|
+
durationSeconds: l.duration_seconds,
|
|
351
|
+
hasResources: l.course_lesson_file.length > 0,
|
|
352
|
+
})),
|
|
353
|
+
})),
|
|
354
|
+
instructor,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async getLessonDetail(lessonId: number) {
|
|
359
|
+
const lesson = await this.prisma.course_lesson.findFirst({
|
|
360
|
+
where: { id: lessonId, published: true },
|
|
361
|
+
select: {
|
|
362
|
+
id: true,
|
|
363
|
+
title: true,
|
|
364
|
+
description: true,
|
|
365
|
+
type: true,
|
|
366
|
+
content: true,
|
|
367
|
+
duration_seconds: true,
|
|
368
|
+
course_lesson_file: {
|
|
369
|
+
select: {
|
|
370
|
+
id: true,
|
|
371
|
+
title: true,
|
|
372
|
+
type: true,
|
|
373
|
+
is_public: true,
|
|
374
|
+
file: { select: { id: true } },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (!lesson) throw new NotFoundException('Lesson not found');
|
|
381
|
+
|
|
382
|
+
const resources = lesson.course_lesson_file.filter(
|
|
383
|
+
(f) => f.file?.id && f.is_public && !f.type?.startsWith('video'),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// HLS takes priority: if a video_hls file exists, use it exclusively
|
|
387
|
+
const hlsFile = lesson.course_lesson_file.find((f) => f.file?.id && f.type === 'video_hls');
|
|
388
|
+
if (hlsFile) {
|
|
389
|
+
return {
|
|
390
|
+
id: lesson.id,
|
|
391
|
+
title: lesson.title,
|
|
392
|
+
description: lesson.description,
|
|
393
|
+
type: lesson.type,
|
|
394
|
+
content: lesson.content,
|
|
395
|
+
durationSeconds: lesson.duration_seconds,
|
|
396
|
+
videos: [{ type: 'video_hls', label: 'HLS', fileId: hlsFile.file!.id! }],
|
|
397
|
+
resources: resources.map((r) => ({
|
|
398
|
+
id: r.id,
|
|
399
|
+
title: r.title,
|
|
400
|
+
url: r.file?.id ? `/file/open/${r.file.id}` : null,
|
|
401
|
+
})),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Legacy MP4 fallback
|
|
406
|
+
const videoFiles = lesson.course_lesson_file.filter(
|
|
407
|
+
(f) => f.file?.id && (f.type === 'video_original' || f.type?.startsWith('video_profile:')),
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
id: lesson.id,
|
|
412
|
+
title: lesson.title,
|
|
413
|
+
description: lesson.description,
|
|
414
|
+
type: lesson.type,
|
|
415
|
+
content: lesson.content,
|
|
416
|
+
durationSeconds: lesson.duration_seconds,
|
|
417
|
+
videos: videoFiles.map((f) => {
|
|
418
|
+
const profileMatch = f.type?.match(/^video_profile:(\d+)$/);
|
|
419
|
+
const profileId = profileMatch ? Number(profileMatch[1]) : null;
|
|
420
|
+
const label =
|
|
421
|
+
f.type === 'video_original'
|
|
422
|
+
? 'Original'
|
|
423
|
+
: profileId !== null
|
|
424
|
+
? f.title || `Perfil ${profileId}`
|
|
425
|
+
: f.title || f.type || 'Vídeo';
|
|
426
|
+
return { type: f.type!, label, fileId: f.file!.id! };
|
|
427
|
+
}),
|
|
428
|
+
resources: resources.map((r) => ({
|
|
429
|
+
id: r.id,
|
|
430
|
+
title: r.title,
|
|
431
|
+
url: r.file?.id ? `/file/open/${r.file.id}` : null,
|
|
432
|
+
})),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
217
436
|
async getDashboard(userId: number, enterpriseId?: number) {
|
|
218
437
|
const personUser = await this.prisma.person_user.findFirst({
|
|
219
438
|
where: { user_id: userId },
|
|
@@ -547,4 +547,127 @@ export class EvaluationService {
|
|
|
547
547
|
await this.prisma.evaluation_topic.delete({ where: { id } });
|
|
548
548
|
return { deleted: true };
|
|
549
549
|
}
|
|
550
|
+
|
|
551
|
+
async getCourseLessonTopics(lessonId: number) {
|
|
552
|
+
const topics = await this.prisma.evaluation_topic.findMany({
|
|
553
|
+
where: {
|
|
554
|
+
target_type: 'course_lesson',
|
|
555
|
+
is_active: true,
|
|
556
|
+
OR: [
|
|
557
|
+
{ course_lesson_id: lessonId },
|
|
558
|
+
{ course_lesson_id: null },
|
|
559
|
+
],
|
|
560
|
+
},
|
|
561
|
+
orderBy: { order: 'asc' },
|
|
562
|
+
select: { id: true, name: true, description: true, order: true },
|
|
563
|
+
});
|
|
564
|
+
return topics.map((t) => ({
|
|
565
|
+
id: t.id,
|
|
566
|
+
name: t.name,
|
|
567
|
+
description: t.description ?? null,
|
|
568
|
+
order: t.order,
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async getMyLessonRatings(lessonId: number, userId: number) {
|
|
573
|
+
const personUser = await this.prisma.person_user.findFirst({
|
|
574
|
+
where: { user_id: userId },
|
|
575
|
+
select: { person_id: true },
|
|
576
|
+
});
|
|
577
|
+
if (!personUser) return [];
|
|
578
|
+
|
|
579
|
+
const topics = await this.prisma.evaluation_topic.findMany({
|
|
580
|
+
where: {
|
|
581
|
+
target_type: 'course_lesson',
|
|
582
|
+
is_active: true,
|
|
583
|
+
OR: [{ course_lesson_id: lessonId }, { course_lesson_id: null }],
|
|
584
|
+
},
|
|
585
|
+
select: { id: true },
|
|
586
|
+
});
|
|
587
|
+
const topicIds = topics.map((t) => t.id);
|
|
588
|
+
if (topicIds.length === 0) return [];
|
|
589
|
+
|
|
590
|
+
const ratings = await this.prisma.evaluation_rating.findMany({
|
|
591
|
+
where: {
|
|
592
|
+
evaluation_topic_id: { in: topicIds },
|
|
593
|
+
evaluator_id: personUser.person_id,
|
|
594
|
+
},
|
|
595
|
+
select: { evaluation_topic_id: true, score: true, comment: true },
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
return ratings.map((r) => ({
|
|
599
|
+
topicId: r.evaluation_topic_id,
|
|
600
|
+
score: Number(r.score),
|
|
601
|
+
comment: r.comment ?? null,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async submitLessonRatings(
|
|
606
|
+
lessonId: number,
|
|
607
|
+
userId: number,
|
|
608
|
+
ratings: Array<{ topicId: number; score: number; comment?: string }>,
|
|
609
|
+
) {
|
|
610
|
+
if (ratings.length === 0) return { saved: 0 };
|
|
611
|
+
|
|
612
|
+
const topicIds = ratings.map((r) => r.topicId);
|
|
613
|
+
const validTopics = await this.prisma.evaluation_topic.findMany({
|
|
614
|
+
where: {
|
|
615
|
+
id: { in: topicIds },
|
|
616
|
+
target_type: 'course_lesson',
|
|
617
|
+
is_active: true,
|
|
618
|
+
OR: [
|
|
619
|
+
{ course_lesson_id: lessonId },
|
|
620
|
+
{ course_lesson_id: null },
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
select: { id: true },
|
|
624
|
+
});
|
|
625
|
+
const validIds = new Set(validTopics.map((t) => t.id));
|
|
626
|
+
|
|
627
|
+
const personUser = await this.prisma.person_user.findFirst({
|
|
628
|
+
where: { user_id: userId },
|
|
629
|
+
select: { person_id: true },
|
|
630
|
+
});
|
|
631
|
+
const personId = personUser?.person_id ?? null;
|
|
632
|
+
|
|
633
|
+
const validRatings = ratings.filter((r) => validIds.has(r.topicId));
|
|
634
|
+
if (validRatings.length === 0) return { saved: 0 };
|
|
635
|
+
|
|
636
|
+
if (personId) {
|
|
637
|
+
await this.prisma.$transaction(async (tx) => {
|
|
638
|
+
for (const r of validRatings) {
|
|
639
|
+
const existing = await tx.evaluation_rating.findFirst({
|
|
640
|
+
where: { evaluation_topic_id: r.topicId, evaluator_id: personId },
|
|
641
|
+
select: { id: true },
|
|
642
|
+
});
|
|
643
|
+
if (existing) {
|
|
644
|
+
await tx.evaluation_rating.update({
|
|
645
|
+
where: { id: existing.id },
|
|
646
|
+
data: { score: r.score, comment: r.comment ?? null },
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
await tx.evaluation_rating.create({
|
|
650
|
+
data: {
|
|
651
|
+
evaluation_topic_id: r.topicId,
|
|
652
|
+
evaluator_id: personId,
|
|
653
|
+
score: r.score,
|
|
654
|
+
comment: r.comment ?? null,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
await this.prisma.evaluation_rating.createMany({
|
|
662
|
+
data: validRatings.map((r) => ({
|
|
663
|
+
evaluation_topic_id: r.topicId,
|
|
664
|
+
evaluator_id: null,
|
|
665
|
+
score: r.score,
|
|
666
|
+
comment: r.comment ?? null,
|
|
667
|
+
})),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return { saved: validRatings.length };
|
|
672
|
+
}
|
|
550
673
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Type } from 'class-transformer';
|
|
2
|
+
import {
|
|
3
|
+
ArrayMinSize,
|
|
4
|
+
IsArray,
|
|
5
|
+
IsBoolean,
|
|
6
|
+
IsIn,
|
|
7
|
+
IsInt,
|
|
8
|
+
IsNotEmpty,
|
|
9
|
+
IsOptional,
|
|
10
|
+
IsString,
|
|
11
|
+
MaxLength,
|
|
12
|
+
Min,
|
|
13
|
+
ValidateNested,
|
|
14
|
+
} from 'class-validator';
|
|
15
|
+
import {
|
|
16
|
+
EXAM_QUESTION_TYPES,
|
|
17
|
+
ExamQuestionAlternativeDto,
|
|
18
|
+
FillBlankAnswerDto,
|
|
19
|
+
MatchingPairDto,
|
|
20
|
+
type ExamQuestionType,
|
|
21
|
+
} from './create-exam-question.dto';
|
|
22
|
+
|
|
23
|
+
export class CreateStandaloneQuestionDto {
|
|
24
|
+
@IsInt()
|
|
25
|
+
@Min(1)
|
|
26
|
+
@IsOptional()
|
|
27
|
+
subjectId?: number;
|
|
28
|
+
|
|
29
|
+
@IsString()
|
|
30
|
+
@IsNotEmpty()
|
|
31
|
+
statement: string;
|
|
32
|
+
|
|
33
|
+
@IsString()
|
|
34
|
+
@IsOptional()
|
|
35
|
+
explanation?: string;
|
|
36
|
+
|
|
37
|
+
@IsString()
|
|
38
|
+
@IsIn(EXAM_QUESTION_TYPES)
|
|
39
|
+
@IsOptional()
|
|
40
|
+
questionType?: ExamQuestionType;
|
|
41
|
+
|
|
42
|
+
@IsInt()
|
|
43
|
+
@Min(1)
|
|
44
|
+
@IsOptional()
|
|
45
|
+
points?: number;
|
|
46
|
+
|
|
47
|
+
@IsArray()
|
|
48
|
+
@ValidateNested({ each: true })
|
|
49
|
+
@Type(() => ExamQuestionAlternativeDto)
|
|
50
|
+
@IsOptional()
|
|
51
|
+
alternatives?: ExamQuestionAlternativeDto[];
|
|
52
|
+
|
|
53
|
+
@IsArray()
|
|
54
|
+
@ArrayMinSize(1)
|
|
55
|
+
@ValidateNested({ each: true })
|
|
56
|
+
@Type(() => FillBlankAnswerDto)
|
|
57
|
+
@IsOptional()
|
|
58
|
+
fillBlankAnswers?: FillBlankAnswerDto[];
|
|
59
|
+
|
|
60
|
+
@IsArray()
|
|
61
|
+
@ArrayMinSize(2)
|
|
62
|
+
@ValidateNested({ each: true })
|
|
63
|
+
@Type(() => MatchingPairDto)
|
|
64
|
+
@IsOptional()
|
|
65
|
+
matchingPairs?: MatchingPairDto[];
|
|
66
|
+
}
|
package/src/exam/exam.module.ts
CHANGED
|
@@ -5,10 +5,11 @@ import { ExamAttemptService } from './exam-attempt.service';
|
|
|
5
5
|
import { LmsExamMcpTools } from './exam.mcp-tools';
|
|
6
6
|
import { ExamController } from './exam.controller';
|
|
7
7
|
import { ExamService } from './exam.service';
|
|
8
|
+
import { QuestionController } from './question.controller';
|
|
8
9
|
|
|
9
10
|
@Module({
|
|
10
11
|
imports: [forwardRef(() => PrismaModule)],
|
|
11
|
-
controllers: [ExamController, ExamAttemptController],
|
|
12
|
+
controllers: [ExamController, ExamAttemptController, QuestionController],
|
|
12
13
|
providers: [ExamService, ExamAttemptService, LmsExamMcpTools],
|
|
13
14
|
exports: [forwardRef(() => ExamService), forwardRef(() => ExamAttemptService)],
|
|
14
15
|
})
|
package/src/exam/exam.service.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
OBJECTIVE_EXAM_QUESTION_TYPES,
|
|
10
10
|
type ExamQuestionType,
|
|
11
11
|
} from './dto/create-exam-question.dto';
|
|
12
|
+
import { CreateStandaloneQuestionDto } from './dto/create-standalone-question.dto';
|
|
12
13
|
import { CreateExamDto } from './dto/create-exam.dto';
|
|
13
14
|
import { CreateQuestionSubjectDto } from './dto/create-question-subject.dto';
|
|
14
15
|
import { UpdateExamQuestionDto } from './dto/update-exam-question.dto';
|
|
@@ -197,6 +198,91 @@ export class ExamService {
|
|
|
197
198
|
return this.mapExam(exam, avgScores[id] ?? 0);
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
async listAllQuestions(search?: string, page = 1, limit = 50) {
|
|
202
|
+
const skip = (Math.max(page, 1) - 1) * limit;
|
|
203
|
+
const where = search?.trim()
|
|
204
|
+
? { statement: { contains: search.trim(), mode: 'insensitive' as const } }
|
|
205
|
+
: {};
|
|
206
|
+
|
|
207
|
+
const [data, total] = await Promise.all([
|
|
208
|
+
this.prisma.question.findMany({
|
|
209
|
+
where,
|
|
210
|
+
skip,
|
|
211
|
+
take: limit,
|
|
212
|
+
orderBy: { id: 'desc' },
|
|
213
|
+
select: {
|
|
214
|
+
id: true,
|
|
215
|
+
question_type: true,
|
|
216
|
+
statement: true,
|
|
217
|
+
points: true,
|
|
218
|
+
question_subject: { select: { id: true, name: true } },
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
this.prisma.question.count({ where }),
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
total,
|
|
226
|
+
page,
|
|
227
|
+
limit,
|
|
228
|
+
data: data.map((q) => ({
|
|
229
|
+
id: q.id,
|
|
230
|
+
questionType: q.question_type,
|
|
231
|
+
statement: q.statement,
|
|
232
|
+
points: q.points,
|
|
233
|
+
subjectId: q.question_subject?.id ?? null,
|
|
234
|
+
subjectName: q.question_subject?.name ?? null,
|
|
235
|
+
})),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async createStandaloneQuestion(dto: CreateStandaloneQuestionDto) {
|
|
240
|
+
const questionType = this.resolveQuestionType(dto.questionType);
|
|
241
|
+
const alternatives = this.normalizeAlternatives(questionType, dto.alternatives, true);
|
|
242
|
+
const fillBlankAnswers = this.normalizeFillBlankAnswers(questionType, dto.fillBlankAnswers, true);
|
|
243
|
+
const matchingPairs = this.normalizeMatchingPairs(questionType, dto.matchingPairs, true);
|
|
244
|
+
const specialOptions = this.buildSpecialOptions(questionType, fillBlankAnswers, matchingPairs);
|
|
245
|
+
|
|
246
|
+
return this.prisma.$transaction(async (tx) => {
|
|
247
|
+
const question = await tx.question.create({
|
|
248
|
+
data: {
|
|
249
|
+
question_subject_id: dto.subjectId ?? null,
|
|
250
|
+
question_type: questionType,
|
|
251
|
+
statement: dto.statement,
|
|
252
|
+
explanation: dto.explanation ?? null,
|
|
253
|
+
points: dto.points ?? 1,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const optionsToPersist =
|
|
258
|
+
alternatives.length > 0
|
|
259
|
+
? alternatives.map((a, i) => ({
|
|
260
|
+
question_id: question.id,
|
|
261
|
+
option_text: a.text,
|
|
262
|
+
is_correct: a.isCorrect,
|
|
263
|
+
position: i,
|
|
264
|
+
}))
|
|
265
|
+
: specialOptions.map((o, i) => ({
|
|
266
|
+
question_id: question.id,
|
|
267
|
+
option_text: o.optionText,
|
|
268
|
+
is_correct: true,
|
|
269
|
+
position: i,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
if (optionsToPersist.length > 0) {
|
|
273
|
+
await tx.exam_option.createMany({ data: optionsToPersist });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
id: question.id,
|
|
278
|
+
questionType,
|
|
279
|
+
statement: question.statement,
|
|
280
|
+
points: question.points,
|
|
281
|
+
subjectId: question.question_subject_id ?? null,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
200
286
|
async listQuestions(examId: number) {
|
|
201
287
|
const exam = await this.prisma.exam.findUnique({
|
|
202
288
|
where: { id: examId },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Role } from '@hed-hog/api';
|
|
2
|
+
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
|
3
|
+
import { CreateStandaloneQuestionDto } from './dto/create-standalone-question.dto';
|
|
4
|
+
import { ExamService } from './exam.service';
|
|
5
|
+
|
|
6
|
+
@Role()
|
|
7
|
+
@Controller('lms/questions')
|
|
8
|
+
export class QuestionController {
|
|
9
|
+
constructor(private readonly examService: ExamService) {}
|
|
10
|
+
|
|
11
|
+
@Get()
|
|
12
|
+
listAll(
|
|
13
|
+
@Query('search') search?: string,
|
|
14
|
+
@Query('page') page?: string,
|
|
15
|
+
@Query('limit') limit?: string,
|
|
16
|
+
) {
|
|
17
|
+
return this.examService.listAllQuestions(
|
|
18
|
+
search,
|
|
19
|
+
page ? Number(page) : 1,
|
|
20
|
+
limit ? Number(limit) : 50,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Post()
|
|
25
|
+
create(@Body() dto: CreateStandaloneQuestionDto) {
|
|
26
|
+
return this.examService.createStandaloneQuestion(dto);
|
|
27
|
+
}
|
|
28
|
+
}
|