@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.
Files changed (218) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  6. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  7. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  8. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  9. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  10. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12.service.js +628 -0
  12. package/dist/course/course-export-scorm12.service.js.map +1 -0
  13. package/dist/course/course-export.service.d.ts +84 -0
  14. package/dist/course/course-export.service.d.ts.map +1 -0
  15. package/dist/course/course-export.service.js +237 -0
  16. package/dist/course/course-export.service.js.map +1 -0
  17. package/dist/course/course-structure.controller.d.ts +17 -9
  18. package/dist/course/course-structure.controller.d.ts.map +1 -1
  19. package/dist/course/course-structure.controller.js +17 -4
  20. package/dist/course/course-structure.controller.js.map +1 -1
  21. package/dist/course/course-structure.service.d.ts +12 -4
  22. package/dist/course/course-structure.service.d.ts.map +1 -1
  23. package/dist/course/course-structure.service.js +98 -23
  24. package/dist/course/course-structure.service.js.map +1 -1
  25. package/dist/course/course-video-hls.service.d.ts +57 -0
  26. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  27. package/dist/course/course-video-hls.service.js +767 -0
  28. package/dist/course/course-video-hls.service.js.map +1 -0
  29. package/dist/course/course.controller.d.ts +45 -13
  30. package/dist/course/course.controller.d.ts.map +1 -1
  31. package/dist/course/course.controller.js +40 -26
  32. package/dist/course/course.controller.js.map +1 -1
  33. package/dist/course/course.mcp-tools.js +1 -1
  34. package/dist/course/course.mcp-tools.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +11 -0
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +6 -9
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +57 -48
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  44. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  48. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  52. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  54. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  55. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  56. package/dist/course/dto/create-course-export.dto.js +71 -0
  57. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  58. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  59. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  61. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  62. package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
  63. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  64. package/dist/course/lms-bulk-upload-automation.service.js +102 -8
  65. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  66. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  67. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  68. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  69. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  70. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  71. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  72. package/dist/course/lms-bulk-upload.controller.js +43 -2
  73. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  74. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  75. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload.service.js +59 -6
  77. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  78. package/dist/course/lms-setting.controller.d.ts +2 -1
  79. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  80. package/dist/course/lms-setting.controller.js +4 -2
  81. package/dist/course/lms-setting.controller.js.map +1 -1
  82. package/dist/course/scorm12-schemas.d.ts +4 -0
  83. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  84. package/dist/course/scorm12-schemas.js +9 -0
  85. package/dist/course/scorm12-schemas.js.map +1 -0
  86. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  87. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  88. package/dist/enterprise/training/training-student.service.js +217 -4
  89. package/dist/enterprise/training/training-student.service.js.map +1 -1
  90. package/dist/evaluation/evaluation.service.d.ts +18 -0
  91. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  92. package/dist/evaluation/evaluation.service.js +125 -0
  93. package/dist/evaluation/evaluation.service.js.map +1 -1
  94. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  95. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  96. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  97. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  98. package/dist/exam/exam.module.d.ts.map +1 -1
  99. package/dist/exam/exam.module.js +2 -1
  100. package/dist/exam/exam.module.js.map +1 -1
  101. package/dist/exam/exam.service.d.ts +21 -0
  102. package/dist/exam/exam.service.d.ts.map +1 -1
  103. package/dist/exam/exam.service.js +80 -0
  104. package/dist/exam/exam.service.js.map +1 -1
  105. package/dist/exam/question.controller.d.ts +27 -0
  106. package/dist/exam/question.controller.d.ts.map +1 -0
  107. package/dist/exam/question.controller.js +53 -0
  108. package/dist/exam/question.controller.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  113. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  114. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  115. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  116. package/dist/lms-commerce-access.subscriber.js +74 -0
  117. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  118. package/dist/lms.module.d.ts.map +1 -1
  119. package/dist/lms.module.js +6 -5
  120. package/dist/lms.module.js.map +1 -1
  121. package/dist/platforma/platforma-video.service.d.ts +39 -0
  122. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  123. package/dist/platforma/platforma-video.service.js +301 -0
  124. package/dist/platforma/platforma-video.service.js.map +1 -0
  125. package/dist/platforma/platforma.controller.d.ts +95 -1
  126. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  127. package/dist/platforma/platforma.controller.js +160 -2
  128. package/dist/platforma/platforma.controller.js.map +1 -1
  129. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  130. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  131. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  132. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  133. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  134. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  135. package/dist/student-xp/student-xp.controller.js +24 -0
  136. package/dist/student-xp/student-xp.controller.js.map +1 -1
  137. package/dist/student-xp/student-xp.service.d.ts +16 -0
  138. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  139. package/dist/student-xp/student-xp.service.js +51 -1
  140. package/dist/student-xp/student-xp.service.js.map +1 -1
  141. package/hedhog/data/evaluation_topic.yaml +17 -0
  142. package/hedhog/data/menu.yaml +0 -17
  143. package/hedhog/data/queue_definition.yaml +48 -0
  144. package/hedhog/data/route.yaml +94 -124
  145. package/hedhog/data/setting_group.yaml +19 -19
  146. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  147. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  148. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  149. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  150. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
  151. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  152. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  153. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  154. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  155. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  156. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  157. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  158. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  159. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  160. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  161. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  162. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  163. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
  164. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  167. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
  168. package/hedhog/frontend/messages/en.json +26 -28
  169. package/hedhog/frontend/messages/pt.json +26 -28
  170. package/hedhog/table/course_export.yaml +62 -0
  171. package/package.json +13 -9
  172. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  173. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  174. package/src/course/course-export-scorm12.service.ts +668 -0
  175. package/src/course/course-export.service.ts +280 -0
  176. package/src/course/course-structure.controller.ts +14 -2
  177. package/src/course/course-structure.service.ts +100 -7
  178. package/src/course/course-video-hls.service.ts +946 -0
  179. package/src/course/course.controller.ts +33 -19
  180. package/src/course/course.mcp-tools.ts +1 -1
  181. package/src/course/course.module.ts +11 -0
  182. package/src/course/course.service.ts +73 -60
  183. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  184. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  185. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  186. package/src/course/dto/create-course-export.dto.ts +56 -0
  187. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  188. package/src/course/lms-bulk-upload-automation.service.ts +153 -6
  189. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  190. package/src/course/lms-bulk-upload.controller.ts +32 -2
  191. package/src/course/lms-bulk-upload.service.ts +70 -7
  192. package/src/course/lms-setting.controller.ts +4 -2
  193. package/src/course/scorm12-schemas.ts +9 -0
  194. package/src/enterprise/training/training-student.service.ts +221 -2
  195. package/src/evaluation/evaluation.service.ts +123 -0
  196. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  197. package/src/exam/exam.module.ts +2 -1
  198. package/src/exam/exam.service.ts +86 -0
  199. package/src/exam/question.controller.ts +28 -0
  200. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  201. package/src/lms-commerce-access.subscriber.ts +88 -0
  202. package/src/lms.module.ts +6 -5
  203. package/src/platforma/platforma-video.service.ts +346 -0
  204. package/src/platforma/platforma.controller.ts +95 -1
  205. package/src/platforma/platforma.service.ts +268 -268
  206. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  207. package/src/student-xp/student-xp.controller.ts +18 -2
  208. package/src/student-xp/student-xp.service.ts +84 -2
  209. package/hedhog/data/video_resolution_profile.yaml +0 -7
  210. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  211. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  212. package/hedhog/table/video_resolution_profile.yaml +0 -18
  213. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  214. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  215. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  216. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  217. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  218. 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: { location: true } },
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?.location ?? logo?.file?.location ?? null,
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
+ }
@@ -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
  })
@@ -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
+ }