@hed-hog/lms 0.0.366 → 0.0.370

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 (169) hide show
  1. package/dist/certificate/certificate.controller.d.ts +1 -1
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +4 -2
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +50 -0
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +73 -0
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/course/course-ai-usage.service.d.ts +58 -0
  10. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  11. package/dist/course/course-ai-usage.service.js +176 -0
  12. package/dist/course/course-ai-usage.service.js.map +1 -0
  13. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  14. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  15. package/dist/course/course-audio-transcription.service.js +381 -29
  16. package/dist/course/course-audio-transcription.service.js.map +1 -1
  17. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  18. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  19. package/dist/course/course-export-scorm12.service.js +141 -6
  20. package/dist/course/course-export-scorm12.service.js.map +1 -1
  21. package/dist/course/course-export.service.d.ts.map +1 -1
  22. package/dist/course/course-export.service.js +2 -1
  23. package/dist/course/course-export.service.js.map +1 -1
  24. package/dist/course/course-lesson.controller.d.ts +25 -3
  25. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  26. package/dist/course/course-lesson.controller.js +71 -8
  27. package/dist/course/course-lesson.controller.js.map +1 -1
  28. package/dist/course/course-structure.controller.d.ts +26 -5
  29. package/dist/course/course-structure.controller.d.ts.map +1 -1
  30. package/dist/course/course-structure.controller.js +31 -1
  31. package/dist/course/course-structure.controller.js.map +1 -1
  32. package/dist/course/course-structure.service.d.ts +37 -5
  33. package/dist/course/course-structure.service.d.ts.map +1 -1
  34. package/dist/course/course-structure.service.js +165 -20
  35. package/dist/course/course-structure.service.js.map +1 -1
  36. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  37. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  38. package/dist/course/course-transcription-translation.service.js +227 -0
  39. package/dist/course/course-transcription-translation.service.js.map +1 -0
  40. package/dist/course/course-video-agent-pipeline.service.js +7 -7
  41. package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
  42. package/dist/course/course.module.d.ts.map +1 -1
  43. package/dist/course/course.module.js +4 -0
  44. package/dist/course/course.module.js.map +1 -1
  45. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  46. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  47. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  48. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  49. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  50. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  51. package/dist/course/dto/create-course-export.dto.js +6 -0
  52. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  53. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  54. package/dist/course/lms-bulk-upload-automation.service.js +26 -13
  55. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  56. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  57. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  58. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  59. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  60. package/dist/course/lms-bulk-upload.service.js +48 -29
  61. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  62. package/dist/course/subtitle.util.d.ts +46 -0
  63. package/dist/course/subtitle.util.d.ts.map +1 -0
  64. package/dist/course/subtitle.util.js +206 -0
  65. package/dist/course/subtitle.util.js.map +1 -0
  66. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  67. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  68. package/dist/enterprise/training/training-student.service.js +197 -10
  69. package/dist/enterprise/training/training-student.service.js.map +1 -1
  70. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  71. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  72. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  73. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  74. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  75. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  76. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  77. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  78. package/dist/lms.module.d.ts.map +1 -1
  79. package/dist/lms.module.js +4 -0
  80. package/dist/lms.module.js.map +1 -1
  81. package/dist/platforma/platforma-performance.service.js +121 -121
  82. package/dist/platforma/platforma-video.service.d.ts +8 -0
  83. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  84. package/dist/platforma/platforma-video.service.js +45 -2
  85. package/dist/platforma/platforma-video.service.js.map +1 -1
  86. package/dist/platforma/platforma.controller.d.ts +99 -1
  87. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  88. package/dist/platforma/platforma.controller.js +111 -2
  89. package/dist/platforma/platforma.controller.js.map +1 -1
  90. package/dist/training/dto/create-training.dto.d.ts +9 -0
  91. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  92. package/dist/training/dto/create-training.dto.js +45 -1
  93. package/dist/training/dto/create-training.dto.js.map +1 -1
  94. package/dist/training/training.controller.d.ts +144 -0
  95. package/dist/training/training.controller.d.ts.map +1 -1
  96. package/dist/training/training.service.d.ts +149 -0
  97. package/dist/training/training.service.d.ts.map +1 -1
  98. package/dist/training/training.service.js +332 -167
  99. package/dist/training/training.service.js.map +1 -1
  100. package/hedhog/data/image_type.yaml +10 -0
  101. package/hedhog/data/route.yaml +251 -0
  102. package/hedhog/data/setting_group.yaml +97 -0
  103. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  104. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  105. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  106. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  107. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  108. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  110. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  111. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  112. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  113. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  114. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  115. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  116. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  117. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  118. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  119. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  120. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  121. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  122. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  123. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  124. package/hedhog/frontend/app/courses/page.tsx.ejs +26 -4
  125. package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
  126. package/hedhog/frontend/messages/en.json +23 -12
  127. package/hedhog/frontend/messages/pt.json +23 -12
  128. package/hedhog/query/triggers.sql +33 -0
  129. package/hedhog/table/course_ai_usage.yaml +46 -0
  130. package/hedhog/table/course_lesson.yaml +3 -0
  131. package/hedhog/table/course_lesson_answer.yaml +37 -0
  132. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  133. package/hedhog/table/learning_path.yaml +6 -0
  134. package/hedhog/table/learning_path_module.yaml +22 -0
  135. package/hedhog/table/learning_path_step.yaml +9 -6
  136. package/hedhog/table/lesson_view_event.yaml +66 -66
  137. package/package.json +9 -9
  138. package/src/certificate/certificate.controller.ts +2 -0
  139. package/src/certificate/certificate.service.ts +99 -0
  140. package/src/course/course-ai-usage.service.ts +221 -0
  141. package/src/course/course-audio-transcription.service.ts +471 -43
  142. package/src/course/course-export-scorm12.service.ts +149 -5
  143. package/src/course/course-export.service.ts +1 -0
  144. package/src/course/course-lesson.controller.ts +59 -6
  145. package/src/course/course-structure.controller.ts +16 -0
  146. package/src/course/course-structure.service.ts +184 -10
  147. package/src/course/course-transcription-translation.service.ts +293 -0
  148. package/src/course/course-video-agent-pipeline.service.ts +471 -471
  149. package/src/course/course.module.ts +4 -0
  150. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  151. package/src/course/dto/create-course-export.dto.ts +6 -0
  152. package/src/course/ffmpeg.util.ts +65 -65
  153. package/src/course/lms-bulk-upload-automation.service.ts +29 -7
  154. package/src/course/lms-bulk-upload.service.ts +20 -1
  155. package/src/course/subtitle.util.ts +220 -0
  156. package/src/enterprise/training/training-student.service.ts +224 -4
  157. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  158. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  159. package/src/lms.module.ts +4 -0
  160. package/src/platforma/dto/heartbeat.dto.ts +30 -30
  161. package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
  162. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
  163. package/src/platforma/platforma-heartbeat.service.ts +33 -33
  164. package/src/platforma/platforma-performance.service.ts +606 -606
  165. package/src/platforma/platforma-search.service.ts +48 -48
  166. package/src/platforma/platforma-video.service.ts +59 -3
  167. package/src/platforma/platforma.controller.ts +88 -0
  168. package/src/training/dto/create-training.dto.ts +36 -0
  169. package/src/training/training.service.ts +360 -163
@@ -302,6 +302,8 @@ export class TrainingStudentService {
302
302
 
303
303
  let completedLessonIds: number[] = [];
304
304
  let progressPercent = 0;
305
+ let lastLessonId: number | null = null;
306
+ let lastPositionSeconds = 0;
305
307
 
306
308
  if (userId) {
307
309
  const personUser = await this.prisma.person_user.findFirst({
@@ -315,11 +317,30 @@ export class TrainingStudentService {
315
317
  });
316
318
  if (enrollment) {
317
319
  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
- });
320
+ const [completedRows, lastProgressRow] = await Promise.all([
321
+ this.prisma.course_lesson_progress.findMany({
322
+ where: { course_enrollment_id: enrollment.id, status: 'completed' },
323
+ select: { course_lesson_id: true },
324
+ }),
325
+ this.prisma.course_lesson_progress.findFirst({
326
+ where: {
327
+ course_enrollment_id: enrollment.id,
328
+ status: { not: 'not_started' },
329
+ },
330
+ orderBy: { updated_at: 'desc' },
331
+ select: {
332
+ course_lesson_id: true,
333
+ video_progress_seconds: true,
334
+ status: true,
335
+ },
336
+ }),
337
+ ]);
322
338
  completedLessonIds = completedRows.map((r) => r.course_lesson_id);
339
+ lastLessonId = lastProgressRow?.course_lesson_id ?? null;
340
+ lastPositionSeconds =
341
+ lastProgressRow?.status === 'in_progress'
342
+ ? (lastProgressRow.video_progress_seconds ?? 0)
343
+ : 0;
323
344
  }
324
345
  }
325
346
  }
@@ -338,6 +359,8 @@ export class TrainingStudentService {
338
359
  categoryLabel: this.formatCategoryLabel(categorySlug),
339
360
  completedLessonIds,
340
361
  progressPercent,
362
+ lastLessonId,
363
+ lastPositionSeconds,
341
364
  modules: row.course_module.map((m) => ({
342
365
  id: m.id,
343
366
  title: m.title,
@@ -355,6 +378,172 @@ export class TrainingStudentService {
355
378
  };
356
379
  }
357
380
 
381
+ async submitLessonAnswer(
382
+ userId: number,
383
+ lessonId: number,
384
+ questionId: number,
385
+ optionId: number | null,
386
+ answerText: string | null,
387
+ ) {
388
+ // Determine correctness from the selected option
389
+ let isCorrect: boolean | null = null;
390
+ if (optionId != null) {
391
+ const option = await (this.prisma as any).exam_option.findUnique({
392
+ where: { id: optionId },
393
+ select: { is_correct: true },
394
+ });
395
+ isCorrect = option?.is_correct ?? null;
396
+ }
397
+
398
+ // Upsert the answer
399
+ await (this.prisma as any).course_lesson_answer.upsert({
400
+ where: {
401
+ user_id_course_lesson_id_question_id: {
402
+ user_id: userId,
403
+ course_lesson_id: lessonId,
404
+ question_id: questionId,
405
+ },
406
+ },
407
+ update: {
408
+ exam_option_id: optionId,
409
+ answer_text: answerText,
410
+ is_correct: isCorrect,
411
+ updated_at: new Date(),
412
+ },
413
+ create: {
414
+ user_id: userId,
415
+ course_lesson_id: lessonId,
416
+ question_id: questionId,
417
+ exam_option_id: optionId,
418
+ answer_text: answerText,
419
+ is_correct: isCorrect,
420
+ },
421
+ });
422
+
423
+ // Check if all questions in this lesson are now answered
424
+ const [totalQuestions, answeredQuestions] = await Promise.all([
425
+ (this.prisma as any).course_lesson_question.count({
426
+ where: { course_lesson_id: lessonId },
427
+ }),
428
+ (this.prisma as any).course_lesson_answer.count({
429
+ where: { user_id: userId, course_lesson_id: lessonId },
430
+ }),
431
+ ]);
432
+
433
+ const allAnswered = totalQuestions > 0 && answeredQuestions >= totalQuestions;
434
+
435
+ if (allAnswered) {
436
+ await this.markLessonComplete(userId, lessonId);
437
+ }
438
+
439
+ return { isCorrect, allAnswered };
440
+ }
441
+
442
+ private async markLessonComplete(userId: number, lessonId: number) {
443
+ const lesson = await (this.prisma as any).course_lesson.findUnique({
444
+ where: { id: lessonId },
445
+ select: { course_module: { select: { course_id: true } } },
446
+ });
447
+
448
+ const personUser = await this.prisma.person_user.findFirst({
449
+ where: { user_id: userId },
450
+ select: { person_id: true },
451
+ });
452
+
453
+ if (!personUser || !lesson) return;
454
+
455
+ const personId = personUser.person_id;
456
+ const courseId: number = lesson.course_module?.course_id;
457
+
458
+ const enrollment = await (this.prisma as any).course_enrollment.findFirst({
459
+ where: { person_id: personId, course_id: courseId, status: { in: ['active', 'completed'] } },
460
+ select: { id: true },
461
+ });
462
+
463
+ if (!enrollment) return;
464
+
465
+ const enrollmentId: number = enrollment.id;
466
+
467
+ const existing = await (this.prisma as any).course_lesson_progress.findUnique({
468
+ where: {
469
+ course_enrollment_id_course_lesson_id: {
470
+ course_enrollment_id: enrollmentId,
471
+ course_lesson_id: lessonId,
472
+ },
473
+ },
474
+ select: { status: true },
475
+ });
476
+
477
+ if (existing?.status === 'completed') return;
478
+
479
+ await (this.prisma as any).course_lesson_progress.upsert({
480
+ where: {
481
+ course_enrollment_id_course_lesson_id: {
482
+ course_enrollment_id: enrollmentId,
483
+ course_lesson_id: lessonId,
484
+ },
485
+ },
486
+ create: {
487
+ course_enrollment_id: enrollmentId,
488
+ course_lesson_id: lessonId,
489
+ status: 'completed',
490
+ progress_percent: 100,
491
+ started_at: new Date(),
492
+ completed_at: new Date(),
493
+ time_spent_seconds: 0,
494
+ },
495
+ update: {
496
+ status: 'completed',
497
+ progress_percent: 100,
498
+ completed_at: new Date(),
499
+ },
500
+ });
501
+
502
+ // Sync course-level progress
503
+ const [totalLessons, completedLessons] = await Promise.all([
504
+ (this.prisma as any).course_lesson.count({
505
+ where: { published: true, course_module: { course_id: courseId } },
506
+ }),
507
+ (this.prisma as any).course_lesson_progress.count({
508
+ where: {
509
+ course_enrollment_id: enrollmentId,
510
+ status: 'completed',
511
+ course_lesson: { published: true, course_module: { course_id: courseId } },
512
+ },
513
+ }),
514
+ ]);
515
+
516
+ const progressPercent =
517
+ totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0;
518
+
519
+ await (this.prisma as any).course_enrollment.update({
520
+ where: { id: enrollmentId },
521
+ data: {
522
+ progress_percent: progressPercent,
523
+ ...(progressPercent === 100 ? { status: 'completed', completed_at: new Date() } : {}),
524
+ },
525
+ });
526
+ }
527
+
528
+ async getLessonStudentAnswers(userId: number, lessonId: number) {
529
+ const rows = await (this.prisma as any).course_lesson_answer.findMany({
530
+ where: { user_id: userId, course_lesson_id: lessonId },
531
+ select: {
532
+ question_id: true,
533
+ exam_option_id: true,
534
+ answer_text: true,
535
+ is_correct: true,
536
+ },
537
+ });
538
+
539
+ return rows as Array<{
540
+ question_id: number;
541
+ exam_option_id: number | null;
542
+ answer_text: string | null;
543
+ is_correct: boolean | null;
544
+ }>;
545
+ }
546
+
358
547
  async getLessonDetail(lessonId: number) {
359
548
  const lesson = await this.prisma.course_lesson.findFirst({
360
549
  where: { id: lessonId, published: true },
@@ -383,6 +572,35 @@ export class TrainingStudentService {
383
572
  (f) => f.file?.id && f.is_public && !f.type?.startsWith('video'),
384
573
  );
385
574
 
575
+ const lessonQuestions = lesson.type === 'quiz'
576
+ ? await this.prisma.course_lesson_question.findMany({
577
+ where: { course_lesson_id: lessonId },
578
+ orderBy: { order: 'asc' },
579
+ include: {
580
+ question: {
581
+ include: {
582
+ exam_option: { orderBy: { position: 'asc' } },
583
+ },
584
+ },
585
+ },
586
+ })
587
+ : [];
588
+
589
+ const questions = lessonQuestions.map((lq) => ({
590
+ id: lq.question.id,
591
+ questionType: lq.question.question_type,
592
+ statement: lq.question.statement,
593
+ explanation: lq.question.explanation,
594
+ points: lq.question.points,
595
+ options: lq.question.exam_option.map((o) => ({
596
+ id: o.id,
597
+ optionText: o.option_text,
598
+ isCorrect: o.is_correct,
599
+ feedback: o.feedback,
600
+ position: o.position,
601
+ })),
602
+ }));
603
+
386
604
  // HLS takes priority: if a video_hls file exists, use it exclusively
387
605
  const hlsFile = lesson.course_lesson_file.find((f) => f.file?.id && f.type === 'video_hls');
388
606
  if (hlsFile) {
@@ -399,6 +617,7 @@ export class TrainingStudentService {
399
617
  title: r.title,
400
618
  url: r.file?.id ? `/file/open/${r.file.id}` : null,
401
619
  })),
620
+ questions,
402
621
  };
403
622
  }
404
623
 
@@ -430,6 +649,7 @@ export class TrainingStudentService {
430
649
  title: r.title,
431
650
  url: r.file?.id ? `/file/open/${r.file.id}` : null,
432
651
  })),
652
+ questions,
433
653
  };
434
654
  }
435
655
 
@@ -10,6 +10,7 @@ import {
10
10
  OnModuleInit,
11
11
  } from '@nestjs/common';
12
12
  import axios from 'axios';
13
+ import { CourseAiUsageService } from '../course/course-ai-usage.service';
13
14
 
14
15
  type XpSegmentAiResult = {
15
16
  startSeconds: number;
@@ -44,6 +45,8 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
44
45
  private readonly registry: QueueHandlerRegistry,
45
46
  @Inject(forwardRef(() => DatabaseQueueProvider))
46
47
  private readonly dbQueue: DatabaseQueueProvider,
48
+ @Inject(forwardRef(() => CourseAiUsageService))
49
+ private readonly aiUsageService: CourseAiUsageService,
47
50
  ) {}
48
51
 
49
52
  onModuleInit() {
@@ -295,6 +298,17 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
295
298
  return;
296
299
  }
297
300
 
301
+ // Record AI cost (gpt-4o, billed per token).
302
+ const xpUsage = response.data?.usage ?? {};
303
+ await this.aiUsageService.recordChatUsage({
304
+ lessonId,
305
+ jobType: 'xp_calculation',
306
+ provider: 'openai',
307
+ model: 'gpt-4o',
308
+ inputTokens: Number(xpUsage.prompt_tokens ?? 0),
309
+ outputTokens: Number(xpUsage.completion_tokens ?? 0),
310
+ });
311
+
298
312
  const rawSegments: any[] = Array.isArray(parsed.segments) ? parsed.segments : [];
299
313
  const areaIds = new Set(areas.map((a) => a.id));
300
314
  const skillIds = new Set(skills.map((s) => s.id));
@@ -2,6 +2,7 @@ import { PrismaModule } from '@hed-hog/api-prisma';
2
2
  import { CoreModule } from '@hed-hog/core';
3
3
  import { QueueModule } from '@hed-hog/queue';
4
4
  import { forwardRef, Module } from '@nestjs/common';
5
+ import { CourseAiUsageService } from '../course/course-ai-usage.service';
5
6
  import { LessonXpAiCalculationService } from './lesson-xp-ai-calculation.service';
6
7
  import { LessonXpMapController } from './lesson-xp-map.controller';
7
8
  import { LessonXpMapService } from './lesson-xp-map.service';
@@ -15,7 +16,7 @@ import { LessonXpSegmentService } from './lesson-xp-segment.service';
15
16
  forwardRef(() => QueueModule),
16
17
  ],
17
18
  controllers: [LessonXpMapController, LessonXpSegmentController],
18
- providers: [LessonXpMapService, LessonXpSegmentService, LessonXpAiCalculationService],
19
+ providers: [LessonXpMapService, LessonXpSegmentService, LessonXpAiCalculationService, CourseAiUsageService],
19
20
  exports: [LessonXpMapService, LessonXpSegmentService, LessonXpAiCalculationService],
20
21
  })
21
22
  export class LessonXpMapModule {}
package/src/lms.module.ts CHANGED
@@ -13,7 +13,9 @@ import { ClassGroupModule } from './class-group/class-group.module';
13
13
  import { CourseLessonDiscussionModule } from './course-lesson-discussion/course-lesson-discussion.module';
14
14
  import { CourseLessonNoteModule } from './course-lesson-note/course-lesson-note.module';
15
15
  import { LmsCommerceAccessSubscriber } from './lms-commerce-access.subscriber';
16
+ import { CourseAiUsageService } from './course/course-ai-usage.service';
16
17
  import { CourseAudioTranscriptionService } from './course/course-audio-transcription.service';
18
+ import { CourseTranscriptionTranslationService } from './course/course-transcription-translation.service';
17
19
  import { CourseModule } from './course/course.module';
18
20
  import { LmsDashboardModule } from './dashboard/dashboard.module';
19
21
  import { EnterpriseModule } from './enterprise/enterprise.module';
@@ -67,7 +69,9 @@ import { TrainingModule } from './training/training.module';
67
69
  ],
68
70
  controllers: [PlataformaController],
69
71
  providers: [
72
+ CourseAiUsageService,
70
73
  CourseAudioTranscriptionService,
74
+ CourseTranscriptionTranslationService,
71
75
  PlatformaService,
72
76
  PlatformaVideoService,
73
77
  PlatformaHeartbeatService,
@@ -1,30 +1,30 @@
1
- import { IsBoolean, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
2
-
3
- export class HeartbeatDto {
4
- @IsInt()
5
- lessonId: number;
6
-
7
- @IsInt()
8
- @Min(0)
9
- positionSeconds: number;
10
-
11
- @IsOptional()
12
- @IsString()
13
- sessionId?: string;
14
-
15
- @IsOptional()
16
- @IsInt()
17
- @Min(1)
18
- @Max(10000)
19
- screenWidth?: number;
20
-
21
- @IsOptional()
22
- @IsInt()
23
- @Min(1)
24
- @Max(10000)
25
- screenHeight?: number;
26
-
27
- @IsOptional()
28
- @IsBoolean()
29
- isTouch?: boolean;
30
- }
1
+ import { IsBoolean, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
2
+
3
+ export class HeartbeatDto {
4
+ @IsInt()
5
+ lessonId: number;
6
+
7
+ @IsInt()
8
+ @Min(0)
9
+ positionSeconds: number;
10
+
11
+ @IsOptional()
12
+ @IsString()
13
+ sessionId?: string;
14
+
15
+ @IsOptional()
16
+ @IsInt()
17
+ @Min(1)
18
+ @Max(10000)
19
+ screenWidth?: number;
20
+
21
+ @IsOptional()
22
+ @IsInt()
23
+ @Min(1)
24
+ @Max(10000)
25
+ screenHeight?: number;
26
+
27
+ @IsOptional()
28
+ @IsBoolean()
29
+ isTouch?: boolean;
30
+ }
@@ -1,117 +1,117 @@
1
- import { PrismaService } from '@hed-hog/api-prisma';
2
- import { IJobHandler, QueueHandlerRegistry } from '@hed-hog/queue';
3
- import { Inject, Injectable, Logger, OnModuleInit, forwardRef } from '@nestjs/common';
4
- import { randomBytes } from 'crypto';
5
-
6
- export const EMIT_CERTIFICATE_JOB = 'lms.emit_certificate';
7
-
8
- @Injectable()
9
- export class EmitCertificateHandler implements OnModuleInit, IJobHandler {
10
- private readonly logger = new Logger(EmitCertificateHandler.name);
11
-
12
- constructor(
13
- @Inject(forwardRef(() => PrismaService))
14
- private readonly prisma: PrismaService,
15
- @Inject(forwardRef(() => QueueHandlerRegistry))
16
- private readonly registry: QueueHandlerRegistry,
17
- ) {}
18
-
19
- onModuleInit() {
20
- this.registry.register(EMIT_CERTIFICATE_JOB, this);
21
- this.logger.log(`Registered handler for "${EMIT_CERTIFICATE_JOB}"`);
22
- }
23
-
24
- async handle(job: { payload: Record<string, any> }) {
25
- const { enrollmentId, courseId, personId } = job.payload as {
26
- enrollmentId: number;
27
- courseId: number;
28
- personId: number;
29
- };
30
-
31
- // Idempotency guard — re-entrancy safe
32
- const enrollment = await (this.prisma as any).course_enrollment.findUnique({
33
- where: { id: enrollmentId },
34
- select: { certificate_issued_at: true, completed_at: true, final_score: true },
35
- });
36
-
37
- if (!enrollment) {
38
- this.logger.warn(`Enrollment ${enrollmentId} not found, skipping certificate`);
39
- return { skipped: true };
40
- }
41
-
42
- if (enrollment.certificate_issued_at) {
43
- this.logger.log(`Certificate already issued for enrollment ${enrollmentId}`);
44
- return { skipped: true, alreadyIssued: true };
45
- }
46
-
47
- const [person, course] = await Promise.all([
48
- this.prisma.person.findUnique({
49
- where: { id: personId },
50
- select: { name: true },
51
- }),
52
- (this.prisma as any).course.findUnique({
53
- where: { id: courseId },
54
- select: {
55
- title: true,
56
- certificate_workload: true,
57
- has_certificate: true,
58
- certificate_template_id: true,
59
- },
60
- }),
61
- ]);
62
-
63
- if (!person || !course) {
64
- this.logger.warn(`Person or course not found (enrollment ${enrollmentId})`);
65
- return { skipped: true };
66
- }
67
-
68
- if (!course.has_certificate || !course.certificate_template_id) {
69
- this.logger.log(`Course ${courseId} does not issue certificates`);
70
- return { skipped: true };
71
- }
72
-
73
- // Compute workload hours from lesson durations when not set on the course
74
- let workloadHours: number = course.certificate_workload ?? 0;
75
- if (!workloadHours) {
76
- const agg = await (this.prisma as any).course_lesson.aggregate({
77
- where: { published: true, course_module: { course_id: courseId } },
78
- _sum: { duration_seconds: true },
79
- });
80
- const totalSeconds = (agg._sum?.duration_seconds as number) ?? 0;
81
- workloadHours = Math.max(1, Math.ceil(totalSeconds / 3600));
82
- }
83
-
84
- const issuedAt = new Date();
85
- const verificationCode = randomBytes(12).toString('hex').toUpperCase();
86
-
87
- await (this.prisma as any).certificate.create({
88
- data: {
89
- verification_code: verificationCode,
90
- student_id: personId,
91
- course_enrollment_id: enrollmentId,
92
- course_id: courseId,
93
- certificate_template_id: course.certificate_template_id,
94
- certificate_type: 'course',
95
- student_name: person.name ?? '',
96
- course_name: course.title ?? '',
97
- workload_hours: workloadHours,
98
- issued_at: issuedAt,
99
- completed_at: enrollment.completed_at ?? issuedAt,
100
- final_score: enrollment.final_score ?? null,
101
- public_access: false,
102
- },
103
- });
104
-
105
- // Mark enrollment as issued — prevents duplicate certificates on any future re-run
106
- await (this.prisma as any).course_enrollment.update({
107
- where: { id: enrollmentId },
108
- data: { certificate_issued_at: issuedAt },
109
- });
110
-
111
- this.logger.log(
112
- `Certificate issued — enrollment=${enrollmentId} course=${courseId} code=${verificationCode}`,
113
- );
114
-
115
- return { verificationCode };
116
- }
117
- }
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { IJobHandler, QueueHandlerRegistry } from '@hed-hog/queue';
3
+ import { Inject, Injectable, Logger, OnModuleInit, forwardRef } from '@nestjs/common';
4
+ import { randomBytes } from 'crypto';
5
+
6
+ export const EMIT_CERTIFICATE_JOB = 'lms.emit_certificate';
7
+
8
+ @Injectable()
9
+ export class EmitCertificateHandler implements OnModuleInit, IJobHandler {
10
+ private readonly logger = new Logger(EmitCertificateHandler.name);
11
+
12
+ constructor(
13
+ @Inject(forwardRef(() => PrismaService))
14
+ private readonly prisma: PrismaService,
15
+ @Inject(forwardRef(() => QueueHandlerRegistry))
16
+ private readonly registry: QueueHandlerRegistry,
17
+ ) {}
18
+
19
+ onModuleInit() {
20
+ this.registry.register(EMIT_CERTIFICATE_JOB, this);
21
+ this.logger.log(`Registered handler for "${EMIT_CERTIFICATE_JOB}"`);
22
+ }
23
+
24
+ async handle(job: { payload: Record<string, any> }) {
25
+ const { enrollmentId, courseId, personId } = job.payload as {
26
+ enrollmentId: number;
27
+ courseId: number;
28
+ personId: number;
29
+ };
30
+
31
+ // Idempotency guard — re-entrancy safe
32
+ const enrollment = await (this.prisma as any).course_enrollment.findUnique({
33
+ where: { id: enrollmentId },
34
+ select: { certificate_issued_at: true, completed_at: true, final_score: true },
35
+ });
36
+
37
+ if (!enrollment) {
38
+ this.logger.warn(`Enrollment ${enrollmentId} not found, skipping certificate`);
39
+ return { skipped: true };
40
+ }
41
+
42
+ if (enrollment.certificate_issued_at) {
43
+ this.logger.log(`Certificate already issued for enrollment ${enrollmentId}`);
44
+ return { skipped: true, alreadyIssued: true };
45
+ }
46
+
47
+ const [person, course] = await Promise.all([
48
+ this.prisma.person.findUnique({
49
+ where: { id: personId },
50
+ select: { name: true },
51
+ }),
52
+ (this.prisma as any).course.findUnique({
53
+ where: { id: courseId },
54
+ select: {
55
+ title: true,
56
+ certificate_workload: true,
57
+ has_certificate: true,
58
+ certificate_template_id: true,
59
+ },
60
+ }),
61
+ ]);
62
+
63
+ if (!person || !course) {
64
+ this.logger.warn(`Person or course not found (enrollment ${enrollmentId})`);
65
+ return { skipped: true };
66
+ }
67
+
68
+ if (!course.has_certificate || !course.certificate_template_id) {
69
+ this.logger.log(`Course ${courseId} does not issue certificates`);
70
+ return { skipped: true };
71
+ }
72
+
73
+ // Compute workload hours from lesson durations when not set on the course
74
+ let workloadHours: number = course.certificate_workload ?? 0;
75
+ if (!workloadHours) {
76
+ const agg = await (this.prisma as any).course_lesson.aggregate({
77
+ where: { published: true, course_module: { course_id: courseId } },
78
+ _sum: { duration_seconds: true },
79
+ });
80
+ const totalSeconds = (agg._sum?.duration_seconds as number) ?? 0;
81
+ workloadHours = Math.max(1, Math.ceil(totalSeconds / 3600));
82
+ }
83
+
84
+ const issuedAt = new Date();
85
+ const verificationCode = randomBytes(12).toString('hex').toUpperCase();
86
+
87
+ await (this.prisma as any).certificate.create({
88
+ data: {
89
+ verification_code: verificationCode,
90
+ student_id: personId,
91
+ course_enrollment_id: enrollmentId,
92
+ course_id: courseId,
93
+ certificate_template_id: course.certificate_template_id,
94
+ certificate_type: 'course',
95
+ student_name: person.name ?? '',
96
+ course_name: course.title ?? '',
97
+ workload_hours: workloadHours,
98
+ issued_at: issuedAt,
99
+ completed_at: enrollment.completed_at ?? issuedAt,
100
+ final_score: enrollment.final_score ?? null,
101
+ public_access: false,
102
+ },
103
+ });
104
+
105
+ // Mark enrollment as issued — prevents duplicate certificates on any future re-run
106
+ await (this.prisma as any).course_enrollment.update({
107
+ where: { id: enrollmentId },
108
+ data: { certificate_issued_at: issuedAt },
109
+ });
110
+
111
+ this.logger.log(
112
+ `Certificate issued — enrollment=${enrollmentId} course=${courseId} code=${verificationCode}`,
113
+ );
114
+
115
+ return { verificationCode };
116
+ }
117
+ }