@hed-hog/lms 0.0.361 → 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 (434) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +66 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +91 -0
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
  6. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  7. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
  8. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
  9. package/dist/class-group/class-group.controller.d.ts +16 -16
  10. package/dist/class-group/class-group.service.d.ts +12 -12
  11. package/dist/course/course-audio-transcription.service.d.ts +3 -2
  12. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  13. package/dist/course/course-audio-transcription.service.js +49 -8
  14. package/dist/course/course-audio-transcription.service.js.map +1 -1
  15. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  16. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  17. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  18. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  19. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  20. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  21. package/dist/course/course-export-scorm12.service.js +628 -0
  22. package/dist/course/course-export-scorm12.service.js.map +1 -0
  23. package/dist/course/course-export.service.d.ts +84 -0
  24. package/dist/course/course-export.service.d.ts.map +1 -0
  25. package/dist/course/course-export.service.js +237 -0
  26. package/dist/course/course-export.service.js.map +1 -0
  27. package/dist/course/course-lesson.controller.d.ts +4 -0
  28. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  29. package/dist/course/course-lesson.controller.js +10 -0
  30. package/dist/course/course-lesson.controller.js.map +1 -1
  31. package/dist/course/course-structure.controller.d.ts +24 -9
  32. package/dist/course/course-structure.controller.d.ts.map +1 -1
  33. package/dist/course/course-structure.controller.js +30 -3
  34. package/dist/course/course-structure.controller.js.map +1 -1
  35. package/dist/course/course-structure.service.d.ts +25 -3
  36. package/dist/course/course-structure.service.d.ts.map +1 -1
  37. package/dist/course/course-structure.service.js +234 -24
  38. package/dist/course/course-structure.service.js.map +1 -1
  39. package/dist/course/course-video-conversion.service.d.ts +8 -0
  40. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  41. package/dist/course/course-video-conversion.service.js +87 -51
  42. package/dist/course/course-video-conversion.service.js.map +1 -1
  43. package/dist/course/course-video-hls.service.d.ts +57 -0
  44. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  45. package/dist/course/course-video-hls.service.js +767 -0
  46. package/dist/course/course-video-hls.service.js.map +1 -0
  47. package/dist/course/course.controller.d.ts +115 -11
  48. package/dist/course/course.controller.d.ts.map +1 -1
  49. package/dist/course/course.controller.js +66 -28
  50. package/dist/course/course.controller.js.map +1 -1
  51. package/dist/course/course.mcp-tools.js +1 -1
  52. package/dist/course/course.mcp-tools.js.map +1 -1
  53. package/dist/course/course.module.d.ts.map +1 -1
  54. package/dist/course/course.module.js +13 -0
  55. package/dist/course/course.module.js.map +1 -1
  56. package/dist/course/course.service.d.ts +112 -11
  57. package/dist/course/course.service.d.ts.map +1 -1
  58. package/dist/course/course.service.js +682 -72
  59. package/dist/course/course.service.js.map +1 -1
  60. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  61. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  62. package/dist/course/dto/cleanup-course-storage.dto.js +34 -0
  63. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  64. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  65. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  66. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  67. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  68. package/dist/course/dto/create-course-bulk-job.dto.d.ts +5 -0
  69. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  70. package/dist/course/dto/create-course-bulk-job.dto.js +26 -0
  71. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  72. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  73. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  74. package/dist/course/dto/create-course-export.dto.js +71 -0
  75. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  76. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  77. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  78. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  79. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  80. package/dist/course/lms-bulk-upload-automation.service.d.ts +54 -0
  81. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  82. package/dist/course/lms-bulk-upload-automation.service.js +537 -0
  83. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  84. package/dist/course/lms-bulk-upload-infra.service.d.ts +32 -0
  85. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  86. package/dist/course/lms-bulk-upload-infra.service.js +301 -0
  87. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  88. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  89. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  90. package/dist/course/lms-bulk-upload.constants.js +7 -0
  91. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  92. package/dist/course/lms-bulk-upload.controller.d.ts +144 -1
  93. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  94. package/dist/course/lms-bulk-upload.controller.js +114 -4
  95. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  96. package/dist/course/lms-bulk-upload.service.d.ts +153 -3
  97. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  98. package/dist/course/lms-bulk-upload.service.js +659 -21
  99. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  100. package/dist/course/lms-setting.controller.d.ts +6 -2
  101. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  102. package/dist/course/lms-setting.controller.js +25 -8
  103. package/dist/course/lms-setting.controller.js.map +1 -1
  104. package/dist/course/scorm12-schemas.d.ts +4 -0
  105. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  106. package/dist/course/scorm12-schemas.js +9 -0
  107. package/dist/course/scorm12-schemas.js.map +1 -0
  108. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  109. package/dist/enterprise/enterprise.service.d.ts +20 -20
  110. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  111. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  112. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  113. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  114. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  115. package/dist/enterprise/training/training-student.service.d.ts +52 -1
  116. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  117. package/dist/enterprise/training/training-student.service.js +217 -4
  118. package/dist/enterprise/training/training-student.service.js.map +1 -1
  119. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  120. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  121. package/dist/evaluation/evaluation.service.d.ts +26 -8
  122. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  123. package/dist/evaluation/evaluation.service.js +125 -0
  124. package/dist/evaluation/evaluation.service.js.map +1 -1
  125. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  126. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  127. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  128. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  129. package/dist/exam/exam.module.d.ts.map +1 -1
  130. package/dist/exam/exam.module.js +2 -1
  131. package/dist/exam/exam.module.js.map +1 -1
  132. package/dist/exam/exam.service.d.ts +21 -0
  133. package/dist/exam/exam.service.d.ts.map +1 -1
  134. package/dist/exam/exam.service.js +80 -0
  135. package/dist/exam/exam.service.js.map +1 -1
  136. package/dist/exam/question.controller.d.ts +27 -0
  137. package/dist/exam/question.controller.d.ts.map +1 -0
  138. package/dist/exam/question.controller.js +53 -0
  139. package/dist/exam/question.controller.js.map +1 -0
  140. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  141. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  142. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  143. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  144. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  145. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  146. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  147. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  148. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  149. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  150. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  151. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  152. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  153. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  154. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  155. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  156. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  157. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  158. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  159. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  160. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +30 -0
  161. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  162. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +440 -0
  163. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  164. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  165. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  166. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  167. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  168. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  169. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  170. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  171. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  172. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  173. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  174. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  175. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  176. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  177. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  178. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  179. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  180. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  181. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  182. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  183. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  184. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  185. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  186. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  187. package/dist/lms-commerce-access.subscriber.js +74 -0
  188. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  189. package/dist/lms.module.d.ts.map +1 -1
  190. package/dist/lms.module.js +21 -5
  191. package/dist/lms.module.js.map +1 -1
  192. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  193. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  194. package/dist/platforma/dto/update-profile.dto.js +87 -0
  195. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  196. package/dist/platforma/platforma-video.service.d.ts +39 -0
  197. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  198. package/dist/platforma/platforma-video.service.js +301 -0
  199. package/dist/platforma/platforma-video.service.js.map +1 -0
  200. package/dist/platforma/platforma.controller.d.ts +182 -1
  201. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  202. package/dist/platforma/platforma.controller.js +243 -2
  203. package/dist/platforma/platforma.controller.js.map +1 -1
  204. package/dist/platforma/platforma.service.d.ts +27 -0
  205. package/dist/platforma/platforma.service.d.ts.map +1 -0
  206. package/dist/platforma/platforma.service.js +274 -0
  207. package/dist/platforma/platforma.service.js.map +1 -0
  208. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  209. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  210. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  211. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  212. package/dist/student-xp/student-xp.controller.d.ts +56 -0
  213. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  214. package/dist/student-xp/student-xp.controller.js +138 -0
  215. package/dist/student-xp/student-xp.controller.js.map +1 -0
  216. package/dist/student-xp/student-xp.module.d.ts +3 -0
  217. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  218. package/dist/student-xp/student-xp.module.js +25 -0
  219. package/dist/student-xp/student-xp.module.js.map +1 -0
  220. package/dist/student-xp/student-xp.service.d.ts +81 -0
  221. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  222. package/dist/student-xp/student-xp.service.js +247 -0
  223. package/dist/student-xp/student-xp.service.js.map +1 -0
  224. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  225. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  226. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  227. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  228. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  229. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  230. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  231. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  232. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  233. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  234. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  235. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  236. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  237. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  238. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  239. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  240. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  241. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  242. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  243. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  244. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  245. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  246. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  247. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  248. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  249. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  250. package/dist/xp-catalog/xp-area.controller.js +105 -0
  251. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  252. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  253. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  254. package/dist/xp-catalog/xp-area.service.js +168 -0
  255. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  256. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  257. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  258. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  259. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  260. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  261. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  262. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  263. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  264. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  265. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  266. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  267. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  268. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  269. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  270. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  271. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  272. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  273. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  274. package/dist/xp-catalog/xp-skill.service.js +174 -0
  275. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  276. package/hedhog/data/evaluation_topic.yaml +17 -0
  277. package/hedhog/data/menu.yaml +91 -7
  278. package/hedhog/data/queue_definition.yaml +48 -0
  279. package/hedhog/data/route.yaml +511 -29
  280. package/hedhog/data/setting_group.yaml +20 -20
  281. package/hedhog/data/xp_area.yaml +164 -0
  282. package/hedhog/data/xp_learning_type.yaml +131 -0
  283. package/hedhog/data/xp_skill.yaml +1834 -0
  284. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  285. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  286. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1749 -0
  287. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  288. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  289. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  290. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  291. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  292. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +73 -8
  293. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  294. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  295. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  296. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1172 -0
  297. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  298. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  299. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  300. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  301. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +87 -46
  302. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  303. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  304. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  305. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +618 -480
  306. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +672 -737
  307. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  308. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  309. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  310. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  311. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +24 -10
  312. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  313. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  314. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  315. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  316. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  317. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  318. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  319. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +53 -0
  320. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  321. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +80 -1
  322. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  323. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  324. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  325. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  326. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  327. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  328. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  329. package/hedhog/frontend/app/courses/page.tsx.ejs +445 -230
  330. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  331. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  332. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  333. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  334. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  335. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  336. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  337. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  338. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  339. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  340. package/hedhog/frontend/messages/en.json +412 -31
  341. package/hedhog/frontend/messages/pt.json +412 -31
  342. package/hedhog/table/course_export.yaml +62 -0
  343. package/hedhog/table/lesson_xp_map.yaml +50 -0
  344. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  345. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  346. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  347. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  348. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  349. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  350. package/hedhog/table/student_area_xp.yaml +30 -0
  351. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  352. package/hedhog/table/student_skill_xp.yaml +30 -0
  353. package/hedhog/table/student_xp_event.yaml +34 -0
  354. package/hedhog/table/xp_area.yaml +39 -0
  355. package/hedhog/table/xp_learning_type.yaml +34 -0
  356. package/hedhog/table/xp_skill.yaml +39 -0
  357. package/package.json +13 -8
  358. package/src/bitcode-wallet/bitcode-wallet.service.ts +152 -0
  359. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  360. package/src/course/course-audio-transcription.service.ts +58 -21
  361. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  362. package/src/course/course-export-scorm12.service.ts +668 -0
  363. package/src/course/course-export.service.ts +280 -0
  364. package/src/course/course-lesson.controller.ts +6 -1
  365. package/src/course/course-structure.controller.ts +23 -1
  366. package/src/course/course-structure.service.ts +273 -7
  367. package/src/course/course-video-conversion.service.ts +113 -75
  368. package/src/course/course-video-hls.service.ts +946 -0
  369. package/src/course/course.controller.ts +54 -21
  370. package/src/course/course.mcp-tools.ts +1 -1
  371. package/src/course/course.module.ts +13 -0
  372. package/src/course/course.service.ts +906 -76
  373. package/src/course/dto/cleanup-course-storage.dto.ts +23 -0
  374. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  375. package/src/course/dto/create-course-bulk-job.dto.ts +10 -0
  376. package/src/course/dto/create-course-export.dto.ts +56 -0
  377. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  378. package/src/course/lms-bulk-upload-automation.service.ts +707 -0
  379. package/src/course/lms-bulk-upload-infra.service.ts +360 -0
  380. package/src/course/lms-bulk-upload.constants.ts +5 -0
  381. package/src/course/lms-bulk-upload.controller.ts +110 -4
  382. package/src/course/lms-bulk-upload.service.ts +1092 -204
  383. package/src/course/lms-setting.controller.ts +26 -8
  384. package/src/course/scorm12-schemas.ts +9 -0
  385. package/src/enterprise/training/training-student.service.ts +221 -2
  386. package/src/evaluation/evaluation.service.ts +123 -0
  387. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  388. package/src/exam/exam.module.ts +2 -1
  389. package/src/exam/exam.service.ts +86 -0
  390. package/src/exam/question.controller.ts +28 -0
  391. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  392. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  393. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  394. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  395. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  396. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +570 -0
  397. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  398. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  399. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  400. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  401. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  402. package/src/lms-commerce-access.subscriber.ts +88 -0
  403. package/src/lms.module.ts +21 -5
  404. package/src/platforma/dto/update-profile.dto.ts +59 -0
  405. package/src/platforma/platforma-video.service.ts +346 -0
  406. package/src/platforma/platforma.controller.ts +152 -3
  407. package/src/platforma/platforma.service.ts +268 -0
  408. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  409. package/src/student-xp/student-xp.controller.ts +92 -0
  410. package/src/student-xp/student-xp.module.ts +12 -0
  411. package/src/student-xp/student-xp.service.ts +318 -0
  412. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  413. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  414. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  415. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  416. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  417. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  418. package/src/xp-catalog/xp-area.controller.ts +64 -0
  419. package/src/xp-catalog/xp-area.service.ts +196 -0
  420. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  421. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  422. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  423. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  424. package/src/xp-catalog/xp-skill.service.ts +205 -0
  425. package/hedhog/data/video_resolution_profile.yaml +0 -7
  426. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  427. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  428. package/hedhog/table/video_resolution_profile.yaml +0 -18
  429. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  430. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  431. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  432. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  433. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  434. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -1,10 +1,36 @@
1
1
  import { PrismaService } from '@hed-hog/api-prisma';
2
- import { FileService, IntegrationDeveloperApiService } from '@hed-hog/core';
3
- import { BadRequestException, Inject, Injectable, forwardRef } from '@nestjs/common';
2
+ import {
3
+ FileService,
4
+ IntegrationDeveloperApiService,
5
+ NotificationService,
6
+ } from '@hed-hog/core';
7
+ import { IJobHandler, QueueHandlerRegistry, QueueJobService } from '@hed-hog/queue';
8
+ import {
9
+ BadRequestException,
10
+ Inject,
11
+ Injectable,
12
+ Logger,
13
+ NotFoundException,
14
+ OnModuleInit,
15
+ forwardRef,
16
+ } from '@nestjs/common';
4
17
  import { CourseOperationsIntegrationService } from './course-operations-integration.service';
18
+ import {
19
+ COURSE_STORAGE_CLEANUP_CATEGORIES,
20
+ type CourseStorageCleanupCategory,
21
+ } from './dto/cleanup-course-storage.dto';
5
22
  import { CreateCourseDto } from './dto/create-course.dto';
6
23
  import { UpdateCourseDto } from './dto/update-course.dto';
7
24
 
25
+ export const LMS_COURSE_DELETE_JOB = 'lms.course.delete';
26
+ export const LMS_COURSE_STORAGE_CLEANUP_JOB = 'lms.course.storage.cleanup';
27
+
28
+ type CourseStorageRow = {
29
+ file_id: number | null;
30
+ file_size: number | null;
31
+ storage_category: string;
32
+ };
33
+
8
34
  type CourseImageTypeSlug = 'course-logo' | 'course-banner';
9
35
 
10
36
  type CourseExtraFields = {
@@ -30,15 +56,249 @@ type PersistCourseExtrasInput = {
30
56
  };
31
57
 
32
58
  @Injectable()
33
- export class CourseService {
59
+ export class CourseService implements OnModuleInit, IJobHandler {
60
+ private readonly logger = new Logger(CourseService.name);
61
+
34
62
  constructor(
35
63
  private readonly prisma: PrismaService,
36
64
  private readonly fileService: FileService,
37
65
  @Inject(forwardRef(() => IntegrationDeveloperApiService))
38
66
  private readonly integrationApi: IntegrationDeveloperApiService,
67
+ @Inject(forwardRef(() => QueueJobService))
68
+ private readonly queueJobService: QueueJobService,
69
+ @Inject(forwardRef(() => QueueHandlerRegistry))
70
+ private readonly queueRegistry: QueueHandlerRegistry,
71
+ @Inject(forwardRef(() => NotificationService))
72
+ private readonly notificationService: NotificationService,
39
73
  private readonly operationsIntegration: CourseOperationsIntegrationService,
40
74
  ) {}
41
75
 
76
+ onModuleInit() {
77
+ this.queueRegistry.register(LMS_COURSE_DELETE_JOB, this);
78
+ this.queueRegistry.register(LMS_COURSE_STORAGE_CLEANUP_JOB, this);
79
+ this.logger.log(`Registered handler for "${LMS_COURSE_DELETE_JOB}"`);
80
+ this.logger.log(`Registered handler for "${LMS_COURSE_STORAGE_CLEANUP_JOB}"`);
81
+ }
82
+
83
+ async handle(job: {
84
+ id: number;
85
+ type: string;
86
+ queue_name: string;
87
+ payload: Record<string, any>;
88
+ attempts: number;
89
+ max_attempts: number;
90
+ source_module?: string | null;
91
+ source_entity?: string | null;
92
+ source_entity_id?: string | null;
93
+ }) {
94
+ if (job.type === LMS_COURSE_STORAGE_CLEANUP_JOB) {
95
+ return this.handleStorageCleanupJob(job);
96
+ }
97
+
98
+ if (job.type === LMS_COURSE_DELETE_JOB) {
99
+ return this.handleCourseDeleteJob(job);
100
+ }
101
+
102
+ throw new BadRequestException(`Unsupported job type: ${job.type}`);
103
+ }
104
+
105
+ private async handleCourseDeleteJob(job: {
106
+ id: number;
107
+ type: string;
108
+ queue_name: string;
109
+ payload: Record<string, any>;
110
+ attempts: number;
111
+ max_attempts: number;
112
+ source_module?: string | null;
113
+ source_entity?: string | null;
114
+ source_entity_id?: string | null;
115
+ }) {
116
+ const { courseId, notificationId, notificationUserId } = job.payload as {
117
+ courseId?: number;
118
+ notificationId?: number;
119
+ notificationUserId?: number;
120
+ };
121
+
122
+ if (!courseId) {
123
+ throw new BadRequestException('Missing courseId in job payload');
124
+ }
125
+
126
+ const notify = async (
127
+ progress: number,
128
+ body: string,
129
+ success?: boolean,
130
+ ) => {
131
+ if (!notificationId || !notificationUserId) return;
132
+
133
+ await this.notificationService
134
+ .updateProgress(notificationUserId, notificationId, {
135
+ progress,
136
+ body,
137
+ ...(success !== undefined ? { success } : {}),
138
+ })
139
+ .catch(() => undefined);
140
+ };
141
+
142
+ try {
143
+ await notify(5, 'Preparando exclusão completa do curso...');
144
+
145
+ const course = await this.prisma.course.findUnique({
146
+ where: { id: courseId },
147
+ select: { id: true, status: true, title: true, slug: true },
148
+ });
149
+
150
+ if (!course) {
151
+ throw new NotFoundException('Course not found');
152
+ }
153
+
154
+ if (course.status !== 'archived') {
155
+ throw new BadRequestException('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED');
156
+ }
157
+
158
+ const impact = await this.getCourseDeletionImpact(courseId);
159
+ await notify(
160
+ 20,
161
+ `Removendo ${impact.deletionImpact.fileCount} arquivo(s) relacionados ao curso...`,
162
+ );
163
+
164
+ if (impact.deletionImpact.fileIds.length > 0) {
165
+ await this.fileService
166
+ .delete('pt', { ids: impact.deletionImpact.fileIds })
167
+ .catch(() => undefined);
168
+ }
169
+
170
+ await notify(75, 'Removendo registros do curso e dependências...');
171
+
172
+ await this.prisma.course.delete({ where: { id: courseId } });
173
+
174
+ await notify(90, 'Finalizando exclusão e publicando evento...');
175
+
176
+ await this.integrationApi
177
+ .publishEvent({
178
+ eventName: 'lms.course.deleted',
179
+ sourceModule: 'lms',
180
+ aggregateType: 'course',
181
+ aggregateId: String(courseId),
182
+ payload: { id: courseId },
183
+ })
184
+ .catch(() => null);
185
+
186
+ await notify(100, 'Curso e todos os arquivos relacionados foram excluídos.', true);
187
+
188
+ return { success: true, courseId };
189
+ } catch (error) {
190
+ const message =
191
+ error instanceof Error && error.message
192
+ ? error.message
193
+ : 'Falha ao excluir o curso.';
194
+ await notify(100, message, false);
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ private async handleStorageCleanupJob(job: {
200
+ id: number;
201
+ type: string;
202
+ queue_name: string;
203
+ payload: Record<string, any>;
204
+ attempts: number;
205
+ max_attempts: number;
206
+ source_module?: string | null;
207
+ source_entity?: string | null;
208
+ source_entity_id?: string | null;
209
+ }) {
210
+ const { courseId, category, notificationId, notificationUserId } =
211
+ job.payload as {
212
+ courseId?: number;
213
+ category?: string;
214
+ notificationId?: number;
215
+ notificationUserId?: number;
216
+ };
217
+
218
+ if (!courseId) {
219
+ throw new BadRequestException('Missing courseId in job payload');
220
+ }
221
+
222
+ if (!this.isSupportedStorageCleanupCategory(category)) {
223
+ throw new BadRequestException('Invalid storage category');
224
+ }
225
+
226
+ const notify = async (
227
+ progress: number,
228
+ body: string,
229
+ success?: boolean,
230
+ ) => {
231
+ if (!notificationId || !notificationUserId) return;
232
+
233
+ await this.notificationService
234
+ .updateProgress(notificationUserId, notificationId, {
235
+ progress,
236
+ body,
237
+ ...(success !== undefined ? { success } : {}),
238
+ })
239
+ .catch(() => undefined);
240
+ };
241
+
242
+ try {
243
+ const categoryLabel = this.getStorageCategoryLabel(category);
244
+ await notify(5, `Preparando limpeza da categoria ${categoryLabel}...`);
245
+
246
+ const impact = await this.getCourseStorageCategoryImpact(courseId, category);
247
+
248
+ if (impact.fileCount === 0) {
249
+ await notify(100, `Nenhum arquivo encontrado em ${categoryLabel}.`, true);
250
+ return {
251
+ success: true,
252
+ status: 'empty',
253
+ courseId,
254
+ category,
255
+ cleanupImpact: impact,
256
+ };
257
+ }
258
+
259
+ await notify(
260
+ 25,
261
+ `Removendo vínculos da categoria ${categoryLabel} (${impact.fileCount} arquivo(s))...`,
262
+ );
263
+
264
+ await this.deleteCourseStorageCategoryLinks(courseId, category);
265
+
266
+ await notify(
267
+ 65,
268
+ `Excluindo ${impact.fileCount} arquivo(s) da categoria ${categoryLabel}...`,
269
+ );
270
+
271
+ if (impact.fileIds.length > 0) {
272
+ await this.fileService.delete('pt', { ids: impact.fileIds }).catch(() => undefined);
273
+ }
274
+
275
+ await notify(
276
+ 100,
277
+ `Categoria ${categoryLabel} limpa com sucesso (${this.formatBytes(impact.totalBytes)} liberados).`,
278
+ true,
279
+ );
280
+
281
+ return {
282
+ success: true,
283
+ status: 'processed',
284
+ courseId,
285
+ category,
286
+ cleanupImpact: {
287
+ fileCount: impact.fileCount,
288
+ totalBytes: impact.totalBytes,
289
+ formattedSize: this.formatBytes(impact.totalBytes),
290
+ },
291
+ };
292
+ } catch (error) {
293
+ const message =
294
+ error instanceof Error && error.message
295
+ ? error.message
296
+ : 'Falha ao limpar a categoria de armazenamento.';
297
+ await notify(100, message, false);
298
+ throw error;
299
+ }
300
+ }
301
+
42
302
  private normalizeLevel(value?: string | null) {
43
303
  if (!value) return undefined;
44
304
  const normalized = String(value).trim().toLowerCase();
@@ -220,10 +480,12 @@ export class CourseService {
220
480
  ]);
221
481
 
222
482
  const courseIds = courses.map((course) => course.id);
223
- const [extrasById, projectLinksById] = await Promise.all([
224
- this.getCourseExtras(courseIds),
225
- this.operationsIntegration.getCourseProjectLinks(courseIds),
226
- ]);
483
+ const [extrasById, projectLinksById, coursesWithRunningJobs] =
484
+ await Promise.all([
485
+ this.getCourseExtras(courseIds),
486
+ this.operationsIntegration.getCourseProjectLinks(courseIds),
487
+ this.getCoursesWithRunningJobs(courseIds),
488
+ ]);
227
489
 
228
490
  return {
229
491
  total,
@@ -236,6 +498,8 @@ export class CourseService {
236
498
  undefined,
237
499
  extrasById.get(c.id),
238
500
  projectLinksById.get(c.id),
501
+ undefined,
502
+ coursesWithRunningJobs.has(c.id),
239
503
  ),
240
504
  ),
241
505
  };
@@ -285,7 +549,16 @@ export class CourseService {
285
549
  },
286
550
  },
287
551
  file: {
288
- select: { id: true, filename: true },
552
+ select: {
553
+ id: true,
554
+ filename: true,
555
+ size: true,
556
+ file_mimetype: {
557
+ select: {
558
+ name: true,
559
+ },
560
+ },
561
+ },
289
562
  },
290
563
  },
291
564
  orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
@@ -386,7 +659,7 @@ export class CourseService {
386
659
  };
387
660
  });
388
661
 
389
- return this.mapCourse(
662
+ const mapped = this.mapCourse(
390
663
  c,
391
664
  {
392
665
  lessonCount,
@@ -399,6 +672,19 @@ export class CourseService {
399
672
  projectLinksById.get(id),
400
673
  lessonInstructors,
401
674
  );
675
+
676
+ const deletionImpact = await this.getCourseDeletionImpact(id);
677
+
678
+ return {
679
+ ...mapped,
680
+ deletionImpact: {
681
+ fileCount: deletionImpact.deletionImpact.fileCount,
682
+ totalBytes: deletionImpact.deletionImpact.totalBytes,
683
+ formattedSize: this.formatBytes(deletionImpact.deletionImpact.totalBytes),
684
+ countsBySource: deletionImpact.deletionImpact.countsBySource,
685
+ enrollmentCount: mapped.enrollmentCount ?? 0,
686
+ },
687
+ };
402
688
  }
403
689
 
404
690
  async create(dto: CreateCourseDto) {
@@ -713,10 +999,10 @@ export class CourseService {
713
999
  return updateResult;
714
1000
  }
715
1001
 
716
- async remove(id: number) {
1002
+ async remove(id: number, userId?: number | null) {
717
1003
  const course = await this.prisma.course.findUnique({
718
1004
  where: { id },
719
- select: { id: true, status: true },
1005
+ select: { id: true, status: true, title: true, slug: true },
720
1006
  });
721
1007
 
722
1008
  if (!course) {
@@ -727,69 +1013,465 @@ export class CourseService {
727
1013
  throw new BadRequestException('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED');
728
1014
  }
729
1015
 
730
- const fileIds = await this.getCourseRelatedFileIds(id);
731
- if (fileIds.length > 0) {
732
- await this.fileService.delete('pt', { ids: fileIds }).catch(() => undefined);
1016
+ if (!userId) {
1017
+ throw new BadRequestException('Authenticated user is required');
733
1018
  }
734
1019
 
735
- await this.prisma.course.delete({ where: { id } });
1020
+ const impact = await this.getCourseDeletionImpact(id);
1021
+
1022
+ const notification = await this.notificationService.create({
1023
+ user_id: userId,
1024
+ title: `Excluindo curso "${course.title ?? course.slug}"`,
1025
+ body: `Preparando remoção de ${impact.deletionImpact.fileCount} arquivo(s) (${this.formatBytes(impact.deletionImpact.totalBytes)}).`,
1026
+ type: 'progress' as any,
1027
+ progress: 1,
1028
+ started_at: new Date().toISOString(),
1029
+ auto_remove: false,
1030
+ action_type: 'url' as any,
1031
+ action_url: '/lms/courses',
1032
+ action_data: {
1033
+ source: 'lms-course-delete',
1034
+ courseId: id,
1035
+ },
1036
+ });
736
1037
 
737
- await this.integrationApi.publishEvent({
738
- eventName: 'lms.course.deleted',
739
- sourceModule: 'lms',
740
- aggregateType: 'course',
741
- aggregateId: String(id),
742
- payload: { id },
743
- }).catch(() => null);
1038
+ const job = await this.queueJobService.enqueue(
1039
+ {
1040
+ type: LMS_COURSE_DELETE_JOB,
1041
+ queueName: LMS_COURSE_DELETE_JOB,
1042
+ payload: {
1043
+ courseId: id,
1044
+ notificationId: notification.id,
1045
+ notificationUserId: userId,
1046
+ },
1047
+ sourceModule: 'lms',
1048
+ sourceEntity: 'course',
1049
+ sourceEntityId: String(id),
1050
+ maxAttempts: 3,
1051
+ },
1052
+ userId,
1053
+ );
744
1054
 
745
- return { success: true };
1055
+ return {
1056
+ success: true,
1057
+ status: 'queued',
1058
+ queueJobId: job.id,
1059
+ notificationId: notification.id,
1060
+ deletionImpact: {
1061
+ fileCount: impact.deletionImpact.fileCount,
1062
+ totalBytes: impact.deletionImpact.totalBytes,
1063
+ formattedSize: this.formatBytes(impact.deletionImpact.totalBytes),
1064
+ },
1065
+ };
746
1066
  }
747
1067
 
748
- private async getCourseRelatedFileIds(courseId: number) {
1068
+ async enqueueStorageCategoryCleanup(
1069
+ courseId: number,
1070
+ category: CourseStorageCleanupCategory,
1071
+ userId?: number | null,
1072
+ ) {
1073
+ if (!userId) {
1074
+ throw new BadRequestException('Authenticated user is required');
1075
+ }
1076
+
1077
+ if (!this.isSupportedStorageCleanupCategory(category)) {
1078
+ throw new BadRequestException('Invalid storage category');
1079
+ }
1080
+
1081
+ const course = await this.prisma.course.findUnique({
1082
+ where: { id: courseId },
1083
+ select: { id: true, title: true, slug: true },
1084
+ });
1085
+
1086
+ if (!course) {
1087
+ throw new NotFoundException('Course not found');
1088
+ }
1089
+
1090
+ const categoryLabel = this.getStorageCategoryLabel(category);
1091
+ const impact = await this.getCourseStorageCategoryImpact(courseId, category);
1092
+
1093
+ if (impact.fileCount === 0) {
1094
+ return {
1095
+ success: true,
1096
+ status: 'empty',
1097
+ cleanupImpact: {
1098
+ fileCount: 0,
1099
+ totalBytes: 0,
1100
+ formattedSize: this.formatBytes(0),
1101
+ },
1102
+ };
1103
+ }
1104
+
1105
+ const notification = await this.notificationService.create({
1106
+ user_id: userId,
1107
+ title: `Limpando armazenamento (${categoryLabel})`,
1108
+ body: `Preparando remoção de ${impact.fileCount} arquivo(s) (${this.formatBytes(impact.totalBytes)}).`,
1109
+ type: 'progress' as any,
1110
+ progress: 1,
1111
+ started_at: new Date().toISOString(),
1112
+ auto_remove: false,
1113
+ action_type: 'url' as any,
1114
+ action_url: `/lms/courses/${courseId}`,
1115
+ action_data: {
1116
+ source: 'lms-course-storage-cleanup',
1117
+ courseId,
1118
+ category,
1119
+ },
1120
+ });
1121
+
1122
+ const job = await this.queueJobService.enqueue(
1123
+ {
1124
+ type: LMS_COURSE_STORAGE_CLEANUP_JOB,
1125
+ queueName: LMS_COURSE_STORAGE_CLEANUP_JOB,
1126
+ payload: {
1127
+ courseId,
1128
+ category,
1129
+ notificationId: notification.id,
1130
+ notificationUserId: userId,
1131
+ },
1132
+ sourceModule: 'lms',
1133
+ sourceEntity: 'course',
1134
+ sourceEntityId: String(courseId),
1135
+ maxAttempts: 3,
1136
+ },
1137
+ userId,
1138
+ );
1139
+
1140
+ return {
1141
+ success: true,
1142
+ status: 'queued',
1143
+ queueJobId: job.id,
1144
+ notificationId: notification.id,
1145
+ category,
1146
+ cleanupImpact: {
1147
+ fileCount: impact.fileCount,
1148
+ totalBytes: impact.totalBytes,
1149
+ formattedSize: this.formatBytes(impact.totalBytes),
1150
+ },
1151
+ };
1152
+ }
1153
+
1154
+ private isSupportedStorageCleanupCategory(
1155
+ value: unknown,
1156
+ ): value is CourseStorageCleanupCategory {
1157
+ return (
1158
+ typeof value === 'string' &&
1159
+ COURSE_STORAGE_CLEANUP_CATEGORIES.includes(
1160
+ value as CourseStorageCleanupCategory,
1161
+ )
1162
+ );
1163
+ }
1164
+
1165
+ private getStorageCategoryLabel(category: CourseStorageCleanupCategory) {
1166
+ const labels: Record<CourseStorageCleanupCategory, string> = {
1167
+ video_original: 'Vídeos originais',
1168
+ video_profile: 'Vídeos convertidos',
1169
+ lesson_audio: 'Áudios',
1170
+ extracted_image: 'Imagens extraídas',
1171
+ student_download: 'Downloads do aluno',
1172
+ supplementary_material: 'Materiais de apoio',
1173
+ course_image: 'Imagens do curso',
1174
+ course_file: 'Arquivos do curso',
1175
+ other_lesson_file: 'Outros arquivos',
1176
+ course_export: 'Arquivos de exportação',
1177
+ };
1178
+
1179
+ return labels[category];
1180
+ }
1181
+
1182
+ private async getCourseStorageCategoryImpact(
1183
+ courseId: number,
1184
+ category: CourseStorageCleanupCategory,
1185
+ ) {
1186
+ const rows = await this.getCourseStorageRows(courseId);
1187
+
1188
+ const uniqueFiles = new Map<number, number>();
1189
+ for (const row of rows) {
1190
+ const rowCategory = String(row.storage_category || 'other_lesson_file');
1191
+ if (rowCategory !== category) continue;
1192
+
1193
+ const fileId = Number(row.file_id ?? 0);
1194
+ if (fileId <= 0) continue;
1195
+
1196
+ if (!uniqueFiles.has(fileId)) {
1197
+ uniqueFiles.set(fileId, Number(row.file_size ?? 0) || 0);
1198
+ }
1199
+ }
1200
+
1201
+ const fileIds = Array.from(uniqueFiles.keys());
1202
+ const totalBytes = Array.from(uniqueFiles.values()).reduce(
1203
+ (sum, size) => sum + size,
1204
+ 0,
1205
+ );
1206
+
1207
+ return {
1208
+ category,
1209
+ fileIds,
1210
+ fileCount: fileIds.length,
1211
+ totalBytes,
1212
+ };
1213
+ }
1214
+
1215
+ private async deleteCourseStorageCategoryLinks(
1216
+ courseId: number,
1217
+ category: CourseStorageCleanupCategory,
1218
+ ) {
1219
+ if (category === 'course_image') {
1220
+ await this.prisma.course_image.deleteMany({ where: { course_id: courseId } });
1221
+ return;
1222
+ }
1223
+
1224
+ if (category === 'course_file') {
1225
+ await this.prisma.course_file.deleteMany({ where: { course_id: courseId } });
1226
+ return;
1227
+ }
1228
+
1229
+ if (category === 'extracted_image') {
1230
+ await this.prisma.$executeRaw`
1231
+ DELETE FROM course_lesson_video_frame clvf
1232
+ USING course_lesson cl, course_module cm
1233
+ WHERE clvf.course_lesson_id = cl.id
1234
+ AND cl.course_module_id = cm.id
1235
+ AND cm.course_id = ${courseId}
1236
+ `;
1237
+ return;
1238
+ }
1239
+
1240
+ if (category === 'video_profile') {
1241
+ await this.prisma.$executeRaw`
1242
+ DELETE FROM course_lesson_file clf
1243
+ USING course_lesson cl, course_module cm
1244
+ WHERE clf.course_lesson_id = cl.id
1245
+ AND cl.course_module_id = cm.id
1246
+ AND cm.course_id = ${courseId}
1247
+ AND clf.type LIKE 'video_profile:%'
1248
+ `;
1249
+ return;
1250
+ }
1251
+
1252
+ if (category === 'other_lesson_file') {
1253
+ await this.prisma.$executeRaw`
1254
+ DELETE FROM course_lesson_file clf
1255
+ USING course_lesson cl, course_module cm
1256
+ WHERE clf.course_lesson_id = cl.id
1257
+ AND cl.course_module_id = cm.id
1258
+ AND cm.course_id = ${courseId}
1259
+ AND (
1260
+ clf.type IS NULL
1261
+ OR (
1262
+ clf.type <> 'video_original'
1263
+ AND clf.type <> 'lesson_audio'
1264
+ AND clf.type <> 'student_download'
1265
+ AND clf.type <> 'supplementary_material'
1266
+ AND clf.type NOT LIKE 'video_profile:%'
1267
+ )
1268
+ )
1269
+ `;
1270
+ return;
1271
+ }
1272
+
1273
+ if (category === 'course_export') {
1274
+ const exports = await this.prisma.course_export.findMany({
1275
+ where: {
1276
+ course_id: courseId,
1277
+ file_id: { not: null },
1278
+ status: { in: ['completed', 'failed'] },
1279
+ },
1280
+ select: { id: true, file_id: true },
1281
+ });
1282
+
1283
+ const fileIds = exports
1284
+ .map((e) => e.file_id)
1285
+ .filter((id): id is number => id != null);
1286
+
1287
+ await this.prisma.course_export.deleteMany({
1288
+ where: { id: { in: exports.map((e) => e.id) } },
1289
+ });
1290
+
1291
+ if (fileIds.length > 0) {
1292
+ await this.fileService.delete('pt', { ids: fileIds }).catch(() => undefined);
1293
+ }
1294
+ return;
1295
+ }
1296
+
1297
+ await this.prisma.course_lesson_file.deleteMany({
1298
+ where: {
1299
+ course_lesson: { course_module: { course_id: courseId } },
1300
+ type: category,
1301
+ },
1302
+ });
1303
+ }
1304
+
1305
+ private async getCourseStorageRows(courseId: number) {
1306
+ return this.prisma.$queryRaw<CourseStorageRow[]>`
1307
+ SELECT files.file_id, files.file_size, files.storage_category
1308
+ FROM (
1309
+ SELECT ci.file_id, f.size AS file_size, 'course_image' AS storage_category
1310
+ FROM course_image ci
1311
+ INNER JOIN file f ON f.id = ci.file_id
1312
+ WHERE ci.course_id = ${courseId}
1313
+
1314
+ UNION ALL
1315
+
1316
+ SELECT cf.file_id, f.size AS file_size, 'course_file' AS storage_category
1317
+ FROM course_file cf
1318
+ INNER JOIN file f ON f.id = cf.file_id
1319
+ WHERE cf.course_id = ${courseId}
1320
+
1321
+ UNION ALL
1322
+
1323
+ SELECT clf.file_id,
1324
+ f.size AS file_size,
1325
+ CASE
1326
+ WHEN clf.type = 'video_original' THEN 'video_original'
1327
+ WHEN clf.type LIKE 'video_profile:%' THEN 'video_profile'
1328
+ WHEN clf.type = 'lesson_audio' THEN 'lesson_audio'
1329
+ WHEN clf.type = 'student_download' THEN 'student_download'
1330
+ WHEN clf.type = 'supplementary_material' THEN 'supplementary_material'
1331
+ ELSE 'other_lesson_file'
1332
+ END AS storage_category
1333
+ FROM course_lesson_file clf
1334
+ INNER JOIN file f ON f.id = clf.file_id
1335
+ INNER JOIN course_lesson cl ON cl.id = clf.course_lesson_id
1336
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1337
+ WHERE cm.course_id = ${courseId}
1338
+
1339
+ UNION ALL
1340
+
1341
+ SELECT clvf.file_id, f.size AS file_size, 'extracted_image' AS storage_category
1342
+ FROM course_lesson_video_frame clvf
1343
+ INNER JOIN file f ON f.id = clvf.file_id
1344
+ INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
1345
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1346
+ WHERE cm.course_id = ${courseId}
1347
+
1348
+ UNION ALL
1349
+
1350
+ SELECT ce.file_id, f.size AS file_size, 'course_export' AS storage_category
1351
+ FROM course_export ce
1352
+ INNER JOIN file f ON f.id = ce.file_id
1353
+ WHERE ce.course_id = ${courseId}
1354
+ AND ce.status = 'completed'
1355
+ AND ce.file_id IS NOT NULL
1356
+ ) AS files
1357
+ WHERE files.file_id IS NOT NULL
1358
+ `;
1359
+ }
1360
+
1361
+ private async getCourseDeletionImpact(courseId: number) {
749
1362
  const rows = await this.prisma.$queryRaw<
750
- Array<{ file_id: number | null }>
1363
+ Array<{
1364
+ file_id: number | null;
1365
+ file_size: number | null;
1366
+ source_type: string;
1367
+ }>
751
1368
  >`
752
- SELECT DISTINCT files.file_id
1369
+ SELECT DISTINCT files.file_id, files.file_size, files.source_type
753
1370
  FROM (
754
- SELECT ci.file_id
1371
+ SELECT ci.file_id, f.size AS file_size, 'course_image' AS source_type
755
1372
  FROM course_image ci
1373
+ INNER JOIN file f ON f.id = ci.file_id
756
1374
  WHERE ci.course_id = ${courseId}
757
1375
 
758
1376
  UNION ALL
759
1377
 
760
- SELECT cf.file_id
1378
+ SELECT cf.file_id, f.size AS file_size, 'course_file' AS source_type
761
1379
  FROM course_file cf
1380
+ INNER JOIN file f ON f.id = cf.file_id
762
1381
  WHERE cf.course_id = ${courseId}
763
1382
 
764
1383
  UNION ALL
765
1384
 
766
- SELECT clf.file_id
1385
+ SELECT clf.file_id, f.size AS file_size, 'course_lesson_file' AS source_type
767
1386
  FROM course_lesson_file clf
1387
+ INNER JOIN file f ON f.id = clf.file_id
768
1388
  INNER JOIN course_lesson cl ON cl.id = clf.course_lesson_id
769
1389
  INNER JOIN course_module cm ON cm.id = cl.course_module_id
770
1390
  WHERE cm.course_id = ${courseId}
771
1391
 
772
1392
  UNION ALL
773
1393
 
774
- SELECT clvf.file_id
1394
+ SELECT clvf.file_id, f.size AS file_size, 'course_lesson_video_frame' AS source_type
775
1395
  FROM course_lesson_video_frame clvf
1396
+ INNER JOIN file f ON f.id = clvf.file_id
776
1397
  INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
777
1398
  INNER JOIN course_module cm ON cm.id = cl.course_module_id
778
1399
  WHERE cm.course_id = ${courseId}
779
1400
 
780
1401
  UNION ALL
781
1402
 
782
- SELECT cgm.file_id
1403
+ SELECT cgm.file_id, f.size AS file_size, 'course_class_group_material' AS source_type
783
1404
  FROM course_class_group_material cgm
1405
+ INNER JOIN file f ON f.id = cgm.file_id
784
1406
  INNER JOIN course_class_group ccg ON ccg.id = cgm.course_class_group_id
785
1407
  WHERE ccg.course_id = ${courseId}
1408
+
1409
+ UNION ALL
1410
+
1411
+ SELECT cln.frame_file_id AS file_id, f.size AS file_size, 'course_lesson_note_frame' AS source_type
1412
+ FROM course_lesson_note cln
1413
+ INNER JOIN file f ON f.id = cln.frame_file_id
1414
+ INNER JOIN course_lesson cl ON cl.id = cln.course_lesson_id
1415
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1416
+ WHERE cm.course_id = ${courseId}
786
1417
  ) AS files
787
1418
  WHERE files.file_id IS NOT NULL
788
1419
  `;
789
1420
 
790
- return rows
791
- .map((row) => Number(row.file_id))
792
- .filter((id) => Number.isInteger(id) && id > 0);
1421
+ const uniqueRows = Array.from(
1422
+ rows.reduce((map, row) => {
1423
+ const fileId = Number(row.file_id ?? 0);
1424
+ if (fileId > 0 && !map.has(fileId)) {
1425
+ map.set(fileId, {
1426
+ fileId,
1427
+ size: Number(row.file_size ?? 0) || 0,
1428
+ sourceType: row.source_type,
1429
+ });
1430
+ }
1431
+ return map;
1432
+ }, new Map<number, { fileId: number; size: number; sourceType: string }>()).values(),
1433
+ );
1434
+
1435
+ const countsBySource = rows.reduce<Record<string, number>>((acc, row) => {
1436
+ const key = String(row.source_type || 'unknown');
1437
+ acc[key] = (acc[key] ?? 0) + 1;
1438
+ return acc;
1439
+ }, {});
1440
+
1441
+ return {
1442
+ deletionImpact: {
1443
+ fileIds: uniqueRows.map((row) => row.fileId),
1444
+ fileCount: uniqueRows.length,
1445
+ totalBytes: uniqueRows.reduce((sum, row) => sum + row.size, 0),
1446
+ countsBySource,
1447
+ },
1448
+ };
1449
+ }
1450
+
1451
+ private formatBytes(bytes: number) {
1452
+ if (!Number.isFinite(bytes) || bytes <= 0) {
1453
+ return '0 B';
1454
+ }
1455
+
1456
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1457
+ let value = bytes;
1458
+ let unitIndex = 0;
1459
+
1460
+ while (value >= 1024 && unitIndex < units.length - 1) {
1461
+ value /= 1024;
1462
+ unitIndex += 1;
1463
+ }
1464
+
1465
+ const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
1466
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
1467
+ }
1468
+
1469
+ private async getCourseRelatedFileIds(courseId: number) {
1470
+ const impact = await this.getCourseDeletionImpact(courseId);
1471
+
1472
+ return impact.deletionImpact.fileIds.filter(
1473
+ (id) => Number.isInteger(id) && id > 0,
1474
+ );
793
1475
  }
794
1476
 
795
1477
  private mapCourse(
@@ -819,6 +1501,7 @@ export class CourseService {
819
1501
  } | null;
820
1502
  } | null;
821
1503
  }>,
1504
+ hasRunningJobs = false,
822
1505
  ) {
823
1506
  const resolvedName = extras?.name ?? c.name ?? null;
824
1507
  const resolvedTitle = this.normalizeOptionalText(c.title) ?? c.slug;
@@ -905,6 +1588,7 @@ export class CourseService {
905
1588
  (c.certificate_template_id
906
1589
  ? String(c.certificate_template_id)
907
1590
  : null),
1591
+ hasRunningJobs,
908
1592
  progressByModule: metrics?.progressByModule ?? [],
909
1593
  instructors: Array.from(
910
1594
  new Map(
@@ -980,6 +1664,25 @@ export class CourseService {
980
1664
  return extrasById;
981
1665
  }
982
1666
 
1667
+ private async getCoursesWithRunningJobs(
1668
+ courseIds: number[],
1669
+ ): Promise<Set<number>> {
1670
+ if (courseIds.length === 0) return new Set();
1671
+
1672
+ const rows = (await this.prisma.$queryRawUnsafe(`
1673
+ SELECT DISTINCT cm.course_id
1674
+ FROM queue_job qj
1675
+ INNER JOIN course_lesson cl ON cl.id = CAST(qj.source_entity_id AS INTEGER)
1676
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1677
+ WHERE qj.source_entity = 'course_lesson'
1678
+ AND qj.source_module = 'lms'
1679
+ AND qj.status IN ('pending', 'scheduled', 'processing', 'retrying')
1680
+ AND cm.course_id IN (${courseIds.join(',')})
1681
+ `)) as Array<{ course_id: number }>;
1682
+
1683
+ return new Set(rows.map((r) => Number(r.course_id)));
1684
+ }
1685
+
983
1686
  private async persistCourseExtras(
984
1687
  id: number,
985
1688
  data: PersistCourseExtrasInput,
@@ -1009,7 +1712,8 @@ export class CourseService {
1009
1712
  }
1010
1713
 
1011
1714
  if (data.offeringType !== undefined) {
1012
- pushSetClause('offering_type', data.offeringType);
1715
+ values.push(data.offeringType);
1716
+ setClauses.push(`offering_type = $${values.length}::course_offering_type_8af4d2739d_enum`);
1013
1717
  }
1014
1718
 
1015
1719
  if (setClauses.length === 0) {
@@ -1026,8 +1730,8 @@ export class CourseService {
1026
1730
  `,
1027
1731
  ...values,
1028
1732
  );
1029
- } catch {
1030
- // Some environments may still be behind the current course schema.
1733
+ } catch (err) {
1734
+ console.error('[persistCourseExtras] failed:', err);
1031
1735
  }
1032
1736
  }
1033
1737
 
@@ -1192,50 +1896,176 @@ export class CourseService {
1192
1896
  });
1193
1897
  }
1194
1898
 
1195
- async getCourseVideoProfiles(courseId: number) {
1196
- const rows = await (this.prisma as any).course_video_resolution_profile.findMany({
1197
- where: { course_id: courseId },
1198
- include: { video_resolution_profile: true },
1199
- orderBy: { video_resolution_profile: { name: 'asc' } },
1200
- });
1899
+ async getCourseContentOverview(courseId: number) {
1900
+ const [
1901
+ moduleCount,
1902
+ lessons,
1903
+ transcriptionRows,
1904
+ xpMapRows,
1905
+ extractedImageCount,
1906
+ resourceFileCount,
1907
+ storageRows,
1908
+ ] = await Promise.all([
1909
+ this.prisma.course_module.count({ where: { course_id: courseId } }),
1910
+ this.prisma.course_lesson.findMany({
1911
+ where: { course_module: { course_id: courseId } },
1912
+ select: { id: true, type: true, content: true, published: true },
1913
+ }),
1914
+ this.prisma.course_lesson_transcription_segment.findMany({
1915
+ where: { course_lesson: { course_module: { course_id: courseId } } },
1916
+ select: { course_lesson_id: true },
1917
+ distinct: ['course_lesson_id'],
1918
+ }),
1919
+ this.prisma.lesson_xp_map.findMany({
1920
+ where: { course_lesson: { course_module: { course_id: courseId } } },
1921
+ select: { course_lesson_id: true },
1922
+ }),
1923
+ this.prisma.course_lesson_video_frame.count({
1924
+ where: { course_lesson: { course_module: { course_id: courseId } } },
1925
+ }),
1926
+ this.prisma.course_lesson_file.count({
1927
+ where: {
1928
+ course_lesson: { course_module: { course_id: courseId } },
1929
+ type: { in: ['student_download', 'supplementary_material'] },
1930
+ },
1931
+ }),
1932
+ this.getCourseStorageRows(courseId),
1933
+ ]);
1201
1934
 
1202
- return (rows as any[]).map((row) => ({
1203
- id: row.video_resolution_profile.id,
1204
- name: row.video_resolution_profile.name,
1205
- ffmpeg_params: row.video_resolution_profile.ffmpeg_params,
1206
- status: row.video_resolution_profile.status,
1207
- }));
1208
- }
1935
+ const transcriptionLessonIds = new Set(
1936
+ transcriptionRows.map((r) => r.course_lesson_id),
1937
+ );
1938
+ const xpLessonIds = new Set(xpMapRows.map((r) => r.course_lesson_id));
1939
+
1940
+ const lessonsByType = { video: 0, questao: 0, post: 0 };
1941
+ let publishedLessonCount = 0;
1942
+ let videoWithTranscription = 0;
1943
+ let videoWithXp = 0;
1944
+
1945
+ const categoryOrder = [
1946
+ 'video_original',
1947
+ 'video_profile',
1948
+ 'lesson_audio',
1949
+ 'extracted_image',
1950
+ 'student_download',
1951
+ 'supplementary_material',
1952
+ 'course_image',
1953
+ 'course_file',
1954
+ 'other_lesson_file',
1955
+ 'course_export',
1956
+ ];
1957
+
1958
+ const uniqueStorageFiles = new Map<number, number>();
1959
+ const uniqueStorageCategoryEntries = new Set<string>();
1960
+ const storageCategoryMap = new Map<
1961
+ string,
1962
+ { key: string; fileCount: number; totalBytes: number }
1963
+ >();
1964
+
1965
+ for (const row of storageRows) {
1966
+ const fileId = Number(row.file_id ?? 0);
1967
+ if (fileId <= 0) continue;
1968
+
1969
+ const size = Number(row.file_size ?? 0) || 0;
1970
+ const category = String(row.storage_category || 'other_lesson_file');
1971
+
1972
+ if (!uniqueStorageFiles.has(fileId)) {
1973
+ uniqueStorageFiles.set(fileId, size);
1974
+ }
1209
1975
 
1210
- async syncCourseVideoProfiles(courseId: number, profileIds: number[]) {
1211
- const linkClient = (this.prisma as any).course_video_resolution_profile;
1976
+ const categoryEntryKey = `${category}:${fileId}`;
1977
+ if (uniqueStorageCategoryEntries.has(categoryEntryKey)) {
1978
+ continue;
1979
+ }
1212
1980
 
1213
- await this.prisma.$transaction(async (tx: any) => {
1214
- await tx.course_video_resolution_profile.deleteMany({
1215
- where: { course_id: courseId },
1216
- });
1981
+ uniqueStorageCategoryEntries.add(categoryEntryKey);
1217
1982
 
1218
- if (profileIds.length > 0) {
1219
- await tx.course_video_resolution_profile.createMany({
1220
- data: profileIds.map((id) => ({
1221
- course_id: courseId,
1222
- video_resolution_profile_id: id,
1223
- })),
1224
- });
1225
- }
1226
- });
1983
+ const current = storageCategoryMap.get(category) ?? {
1984
+ key: category,
1985
+ fileCount: 0,
1986
+ totalBytes: 0,
1987
+ };
1227
1988
 
1228
- return linkClient.findMany({
1229
- where: { course_id: courseId },
1230
- include: { video_resolution_profile: true },
1231
- orderBy: { video_resolution_profile: { name: 'asc' } },
1232
- }).then((rows: any[]) =>
1233
- rows.map((row: any) => ({
1234
- id: row.video_resolution_profile.id,
1235
- name: row.video_resolution_profile.name,
1236
- ffmpeg_params: row.video_resolution_profile.ffmpeg_params,
1237
- status: row.video_resolution_profile.status,
1238
- })),
1989
+ current.fileCount += 1;
1990
+ current.totalBytes += size;
1991
+ storageCategoryMap.set(category, current);
1992
+ }
1993
+
1994
+ const storageCategories = Array.from(storageCategoryMap.values()).sort(
1995
+ (a, b) => {
1996
+ const orderA = categoryOrder.indexOf(a.key);
1997
+ const orderB = categoryOrder.indexOf(b.key);
1998
+
1999
+ if (orderA !== -1 || orderB !== -1) {
2000
+ return (orderA === -1 ? Number.MAX_SAFE_INTEGER : orderA) -
2001
+ (orderB === -1 ? Number.MAX_SAFE_INTEGER : orderB);
2002
+ }
2003
+
2004
+ return b.totalBytes - a.totalBytes;
2005
+ },
1239
2006
  );
2007
+
2008
+ for (const lesson of lessons) {
2009
+ if (lesson.published) publishedLessonCount++;
2010
+
2011
+ let sourceType: string | undefined;
2012
+ try {
2013
+ const parsed = lesson.content
2014
+ ? (JSON.parse(lesson.content as string) as Record<string, unknown>)
2015
+ : null;
2016
+ sourceType =
2017
+ typeof parsed?.sourceType === 'string'
2018
+ ? parsed.sourceType
2019
+ : undefined;
2020
+ } catch {
2021
+ // ignore malformed content
2022
+ }
2023
+
2024
+ let uiType: 'video' | 'questao' | 'post';
2025
+ if (
2026
+ sourceType === 'video' ||
2027
+ sourceType === 'questao' ||
2028
+ sourceType === 'post'
2029
+ ) {
2030
+ uiType = sourceType;
2031
+ } else if (lesson.type === 'video') {
2032
+ uiType = 'video';
2033
+ } else if (lesson.type === 'quiz') {
2034
+ uiType = 'questao';
2035
+ } else {
2036
+ uiType = 'post';
2037
+ }
2038
+
2039
+ lessonsByType[uiType]++;
2040
+
2041
+ if (uiType === 'video') {
2042
+ if (transcriptionLessonIds.has(lesson.id)) videoWithTranscription++;
2043
+ if (xpLessonIds.has(lesson.id)) videoWithXp++;
2044
+ }
2045
+ }
2046
+
2047
+ return {
2048
+ structure: {
2049
+ moduleCount,
2050
+ lessonCount: lessons.length,
2051
+ publishedLessonCount,
2052
+ lessonsByType,
2053
+ },
2054
+ videos: {
2055
+ lessonCount: lessonsByType.video,
2056
+ withTranscription: videoWithTranscription,
2057
+ withXp: videoWithXp,
2058
+ },
2059
+ media: { extractedImageCount },
2060
+ resources: { fileCount: resourceFileCount },
2061
+ storage: {
2062
+ totalBytes: Array.from(uniqueStorageFiles.values()).reduce(
2063
+ (sum, size) => sum + size,
2064
+ 0,
2065
+ ),
2066
+ totalFileCount: uniqueStorageFiles.size,
2067
+ categories: storageCategories,
2068
+ },
2069
+ };
1240
2070
  }
1241
2071
  }