@hed-hog/lms 0.0.361 → 0.0.364

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 (329) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +65 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -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-lesson.controller.d.ts +4 -0
  16. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  17. package/dist/course/course-lesson.controller.js +10 -0
  18. package/dist/course/course-lesson.controller.js.map +1 -1
  19. package/dist/course/course-structure.controller.d.ts +11 -4
  20. package/dist/course/course-structure.controller.d.ts.map +1 -1
  21. package/dist/course/course-structure.controller.js +14 -0
  22. package/dist/course/course-structure.controller.js.map +1 -1
  23. package/dist/course/course-structure.service.d.ts +15 -1
  24. package/dist/course/course-structure.service.d.ts.map +1 -1
  25. package/dist/course/course-structure.service.js +139 -4
  26. package/dist/course/course-structure.service.js.map +1 -1
  27. package/dist/course/course-video-conversion.service.d.ts +8 -0
  28. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  29. package/dist/course/course-video-conversion.service.js +87 -51
  30. package/dist/course/course-video-conversion.service.js.map +1 -1
  31. package/dist/course/course.controller.d.ts +73 -1
  32. package/dist/course/course.controller.d.ts.map +1 -1
  33. package/dist/course/course.controller.js +27 -3
  34. package/dist/course/course.controller.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +2 -0
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +108 -4
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +631 -30
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  44. package/dist/course/dto/cleanup-course-storage.dto.js +33 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  48. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +4 -0
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  52. package/dist/course/dto/create-course-bulk-job.dto.js +21 -0
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  54. package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
  55. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  56. package/dist/course/lms-bulk-upload-automation.service.js +443 -0
  57. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  58. package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
  59. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  60. package/dist/course/lms-bulk-upload-infra.service.js +277 -0
  61. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  62. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  63. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  64. package/dist/course/lms-bulk-upload.constants.js +7 -0
  65. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  66. package/dist/course/lms-bulk-upload.controller.d.ts +116 -0
  67. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  68. package/dist/course/lms-bulk-upload.controller.js +71 -2
  69. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  70. package/dist/course/lms-bulk-upload.service.d.ts +142 -3
  71. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  72. package/dist/course/lms-bulk-upload.service.js +606 -21
  73. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  74. package/dist/course/lms-setting.controller.d.ts +4 -1
  75. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  76. package/dist/course/lms-setting.controller.js +21 -6
  77. package/dist/course/lms-setting.controller.js.map +1 -1
  78. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  79. package/dist/enterprise/enterprise.service.d.ts +20 -20
  80. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  81. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  82. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  83. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  84. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  85. package/dist/enterprise/training/training-student.service.d.ts +1 -1
  86. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  87. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  88. package/dist/evaluation/evaluation.service.d.ts +8 -8
  89. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  90. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  91. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  92. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  93. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  94. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  95. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  96. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  97. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  98. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  99. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  100. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  101. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  102. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  103. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  104. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  105. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  106. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  107. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  108. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +26 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +304 -0
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  113. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  114. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  115. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  116. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  117. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  118. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  119. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  120. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  121. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  122. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  123. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  124. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  126. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  127. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  128. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  129. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  130. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  131. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  132. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  133. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +17 -2
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  138. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/update-profile.dto.js +87 -0
  140. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  141. package/dist/platforma/platforma.controller.d.ts +88 -1
  142. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  143. package/dist/platforma/platforma.controller.js +85 -2
  144. package/dist/platforma/platforma.controller.js.map +1 -1
  145. package/dist/platforma/platforma.service.d.ts +27 -0
  146. package/dist/platforma/platforma.service.d.ts.map +1 -0
  147. package/dist/platforma/platforma.service.js +274 -0
  148. package/dist/platforma/platforma.service.js.map +1 -0
  149. package/dist/student-xp/student-xp.controller.d.ts +41 -0
  150. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  151. package/dist/student-xp/student-xp.controller.js +114 -0
  152. package/dist/student-xp/student-xp.controller.js.map +1 -0
  153. package/dist/student-xp/student-xp.module.d.ts +3 -0
  154. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  155. package/dist/student-xp/student-xp.module.js +25 -0
  156. package/dist/student-xp/student-xp.module.js.map +1 -0
  157. package/dist/student-xp/student-xp.service.d.ts +65 -0
  158. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  159. package/dist/student-xp/student-xp.service.js +197 -0
  160. package/dist/student-xp/student-xp.service.js.map +1 -0
  161. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  162. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  163. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  164. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  165. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  166. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  167. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  168. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  169. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  170. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  171. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  172. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  173. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  174. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  175. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  176. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  177. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  178. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  179. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  180. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  181. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  182. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  183. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  184. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  185. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  186. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  187. package/dist/xp-catalog/xp-area.controller.js +105 -0
  188. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  189. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  190. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  191. package/dist/xp-catalog/xp-area.service.js +168 -0
  192. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  193. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  194. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  195. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  196. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  197. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  198. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  199. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  200. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  201. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  202. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  203. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  204. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  205. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  206. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  207. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  208. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  209. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  210. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  211. package/dist/xp-catalog/xp-skill.service.js +174 -0
  212. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  213. package/hedhog/data/menu.yaml +101 -0
  214. package/hedhog/data/route.yaml +512 -0
  215. package/hedhog/data/setting_group.yaml +1 -1
  216. package/hedhog/data/xp_area.yaml +164 -0
  217. package/hedhog/data/xp_learning_type.yaml +131 -0
  218. package/hedhog/data/xp_skill.yaml +1834 -0
  219. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  220. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  221. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1453 -0
  222. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  223. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  224. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  225. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  226. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  227. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +4 -4
  228. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  229. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1170 -0
  230. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  231. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  232. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  233. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  234. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +55 -2
  235. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
  236. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
  237. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  238. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  239. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  240. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +21 -1
  241. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  242. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  243. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  244. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +54 -0
  245. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
  246. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  247. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  248. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  249. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  250. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  251. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  252. package/hedhog/frontend/app/courses/page.tsx.ejs +400 -230
  253. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  254. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  255. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  256. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  257. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  258. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  259. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  260. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  261. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  262. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  263. package/hedhog/frontend/messages/en.json +386 -3
  264. package/hedhog/frontend/messages/pt.json +386 -3
  265. package/hedhog/table/lesson_xp_map.yaml +50 -0
  266. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  267. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  268. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  269. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  270. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  271. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  272. package/hedhog/table/student_area_xp.yaml +30 -0
  273. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  274. package/hedhog/table/student_skill_xp.yaml +30 -0
  275. package/hedhog/table/student_xp_event.yaml +34 -0
  276. package/hedhog/table/xp_area.yaml +39 -0
  277. package/hedhog/table/xp_learning_type.yaml +34 -0
  278. package/hedhog/table/xp_skill.yaml +39 -0
  279. package/package.json +8 -7
  280. package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -0
  281. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  282. package/src/course/course-audio-transcription.service.ts +58 -21
  283. package/src/course/course-lesson.controller.ts +6 -1
  284. package/src/course/course-structure.controller.ts +10 -0
  285. package/src/course/course-structure.service.ts +174 -1
  286. package/src/course/course-video-conversion.service.ts +113 -75
  287. package/src/course/course.controller.ts +22 -3
  288. package/src/course/course.module.ts +2 -0
  289. package/src/course/course.service.ts +847 -30
  290. package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
  291. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  292. package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
  293. package/src/course/lms-bulk-upload-automation.service.ts +560 -0
  294. package/src/course/lms-bulk-upload-infra.service.ts +327 -0
  295. package/src/course/lms-bulk-upload.constants.ts +5 -0
  296. package/src/course/lms-bulk-upload.controller.ts +79 -3
  297. package/src/course/lms-bulk-upload.service.ts +1029 -204
  298. package/src/course/lms-setting.controller.ts +22 -6
  299. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  300. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  301. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  302. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  303. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  304. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +396 -0
  305. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  306. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  307. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  308. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  309. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  310. package/src/lms.module.ts +17 -2
  311. package/src/platforma/dto/update-profile.dto.ts +59 -0
  312. package/src/platforma/platforma.controller.ts +57 -2
  313. package/src/platforma/platforma.service.ts +268 -0
  314. package/src/student-xp/student-xp.controller.ts +76 -0
  315. package/src/student-xp/student-xp.module.ts +12 -0
  316. package/src/student-xp/student-xp.service.ts +236 -0
  317. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  318. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  319. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  320. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  321. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  322. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  323. package/src/xp-catalog/xp-area.controller.ts +64 -0
  324. package/src/xp-catalog/xp-area.service.ts +196 -0
  325. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  326. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  327. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  328. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  329. package/src/xp-catalog/xp-skill.service.ts +205 -0
@@ -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();
@@ -285,7 +545,16 @@ export class CourseService {
285
545
  },
286
546
  },
287
547
  file: {
288
- select: { id: true, filename: true },
548
+ select: {
549
+ id: true,
550
+ filename: true,
551
+ size: true,
552
+ file_mimetype: {
553
+ select: {
554
+ name: true,
555
+ },
556
+ },
557
+ },
289
558
  },
290
559
  },
291
560
  orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
@@ -386,7 +655,7 @@ export class CourseService {
386
655
  };
387
656
  });
388
657
 
389
- return this.mapCourse(
658
+ const mapped = this.mapCourse(
390
659
  c,
391
660
  {
392
661
  lessonCount,
@@ -399,6 +668,19 @@ export class CourseService {
399
668
  projectLinksById.get(id),
400
669
  lessonInstructors,
401
670
  );
671
+
672
+ const deletionImpact = await this.getCourseDeletionImpact(id);
673
+
674
+ return {
675
+ ...mapped,
676
+ deletionImpact: {
677
+ fileCount: deletionImpact.deletionImpact.fileCount,
678
+ totalBytes: deletionImpact.deletionImpact.totalBytes,
679
+ formattedSize: this.formatBytes(deletionImpact.deletionImpact.totalBytes),
680
+ countsBySource: deletionImpact.deletionImpact.countsBySource,
681
+ enrollmentCount: mapped.enrollmentCount ?? 0,
682
+ },
683
+ };
402
684
  }
403
685
 
404
686
  async create(dto: CreateCourseDto) {
@@ -713,10 +995,10 @@ export class CourseService {
713
995
  return updateResult;
714
996
  }
715
997
 
716
- async remove(id: number) {
998
+ async remove(id: number, userId?: number | null) {
717
999
  const course = await this.prisma.course.findUnique({
718
1000
  where: { id },
719
- select: { id: true, status: true },
1001
+ select: { id: true, status: true, title: true, slug: true },
720
1002
  });
721
1003
 
722
1004
  if (!course) {
@@ -727,69 +1009,431 @@ export class CourseService {
727
1009
  throw new BadRequestException('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED');
728
1010
  }
729
1011
 
730
- const fileIds = await this.getCourseRelatedFileIds(id);
731
- if (fileIds.length > 0) {
732
- await this.fileService.delete('pt', { ids: fileIds }).catch(() => undefined);
1012
+ if (!userId) {
1013
+ throw new BadRequestException('Authenticated user is required');
733
1014
  }
734
1015
 
735
- await this.prisma.course.delete({ where: { id } });
1016
+ const impact = await this.getCourseDeletionImpact(id);
1017
+
1018
+ const notification = await this.notificationService.create({
1019
+ user_id: userId,
1020
+ title: `Excluindo curso "${course.title ?? course.slug}"`,
1021
+ body: `Preparando remoção de ${impact.deletionImpact.fileCount} arquivo(s) (${this.formatBytes(impact.deletionImpact.totalBytes)}).`,
1022
+ type: 'progress' as any,
1023
+ progress: 1,
1024
+ started_at: new Date().toISOString(),
1025
+ auto_remove: false,
1026
+ action_type: 'url' as any,
1027
+ action_url: '/lms/courses',
1028
+ action_data: {
1029
+ source: 'lms-course-delete',
1030
+ courseId: id,
1031
+ },
1032
+ });
736
1033
 
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);
1034
+ const job = await this.queueJobService.enqueue(
1035
+ {
1036
+ type: LMS_COURSE_DELETE_JOB,
1037
+ queueName: LMS_COURSE_DELETE_JOB,
1038
+ payload: {
1039
+ courseId: id,
1040
+ notificationId: notification.id,
1041
+ notificationUserId: userId,
1042
+ },
1043
+ sourceModule: 'lms',
1044
+ sourceEntity: 'course',
1045
+ sourceEntityId: String(id),
1046
+ maxAttempts: 3,
1047
+ },
1048
+ userId,
1049
+ );
744
1050
 
745
- return { success: true };
1051
+ return {
1052
+ success: true,
1053
+ status: 'queued',
1054
+ queueJobId: job.id,
1055
+ notificationId: notification.id,
1056
+ deletionImpact: {
1057
+ fileCount: impact.deletionImpact.fileCount,
1058
+ totalBytes: impact.deletionImpact.totalBytes,
1059
+ formattedSize: this.formatBytes(impact.deletionImpact.totalBytes),
1060
+ },
1061
+ };
746
1062
  }
747
1063
 
748
- private async getCourseRelatedFileIds(courseId: number) {
1064
+ async enqueueStorageCategoryCleanup(
1065
+ courseId: number,
1066
+ category: CourseStorageCleanupCategory,
1067
+ userId?: number | null,
1068
+ ) {
1069
+ if (!userId) {
1070
+ throw new BadRequestException('Authenticated user is required');
1071
+ }
1072
+
1073
+ if (!this.isSupportedStorageCleanupCategory(category)) {
1074
+ throw new BadRequestException('Invalid storage category');
1075
+ }
1076
+
1077
+ const course = await this.prisma.course.findUnique({
1078
+ where: { id: courseId },
1079
+ select: { id: true, title: true, slug: true },
1080
+ });
1081
+
1082
+ if (!course) {
1083
+ throw new NotFoundException('Course not found');
1084
+ }
1085
+
1086
+ const categoryLabel = this.getStorageCategoryLabel(category);
1087
+ const impact = await this.getCourseStorageCategoryImpact(courseId, category);
1088
+
1089
+ if (impact.fileCount === 0) {
1090
+ return {
1091
+ success: true,
1092
+ status: 'empty',
1093
+ cleanupImpact: {
1094
+ fileCount: 0,
1095
+ totalBytes: 0,
1096
+ formattedSize: this.formatBytes(0),
1097
+ },
1098
+ };
1099
+ }
1100
+
1101
+ const notification = await this.notificationService.create({
1102
+ user_id: userId,
1103
+ title: `Limpando armazenamento (${categoryLabel})`,
1104
+ body: `Preparando remoção de ${impact.fileCount} arquivo(s) (${this.formatBytes(impact.totalBytes)}).`,
1105
+ type: 'progress' as any,
1106
+ progress: 1,
1107
+ started_at: new Date().toISOString(),
1108
+ auto_remove: false,
1109
+ action_type: 'url' as any,
1110
+ action_url: `/lms/courses/${courseId}`,
1111
+ action_data: {
1112
+ source: 'lms-course-storage-cleanup',
1113
+ courseId,
1114
+ category,
1115
+ },
1116
+ });
1117
+
1118
+ const job = await this.queueJobService.enqueue(
1119
+ {
1120
+ type: LMS_COURSE_STORAGE_CLEANUP_JOB,
1121
+ queueName: LMS_COURSE_STORAGE_CLEANUP_JOB,
1122
+ payload: {
1123
+ courseId,
1124
+ category,
1125
+ notificationId: notification.id,
1126
+ notificationUserId: userId,
1127
+ },
1128
+ sourceModule: 'lms',
1129
+ sourceEntity: 'course',
1130
+ sourceEntityId: String(courseId),
1131
+ maxAttempts: 3,
1132
+ },
1133
+ userId,
1134
+ );
1135
+
1136
+ return {
1137
+ success: true,
1138
+ status: 'queued',
1139
+ queueJobId: job.id,
1140
+ notificationId: notification.id,
1141
+ category,
1142
+ cleanupImpact: {
1143
+ fileCount: impact.fileCount,
1144
+ totalBytes: impact.totalBytes,
1145
+ formattedSize: this.formatBytes(impact.totalBytes),
1146
+ },
1147
+ };
1148
+ }
1149
+
1150
+ private isSupportedStorageCleanupCategory(
1151
+ value: unknown,
1152
+ ): value is CourseStorageCleanupCategory {
1153
+ return (
1154
+ typeof value === 'string' &&
1155
+ COURSE_STORAGE_CLEANUP_CATEGORIES.includes(
1156
+ value as CourseStorageCleanupCategory,
1157
+ )
1158
+ );
1159
+ }
1160
+
1161
+ private getStorageCategoryLabel(category: CourseStorageCleanupCategory) {
1162
+ const labels: Record<CourseStorageCleanupCategory, string> = {
1163
+ video_original: 'Vídeos originais',
1164
+ video_profile: 'Vídeos convertidos',
1165
+ lesson_audio: 'Áudios',
1166
+ extracted_image: 'Imagens extraídas',
1167
+ student_download: 'Downloads do aluno',
1168
+ supplementary_material: 'Materiais de apoio',
1169
+ course_image: 'Imagens do curso',
1170
+ course_file: 'Arquivos do curso',
1171
+ other_lesson_file: 'Outros arquivos',
1172
+ };
1173
+
1174
+ return labels[category];
1175
+ }
1176
+
1177
+ private async getCourseStorageCategoryImpact(
1178
+ courseId: number,
1179
+ category: CourseStorageCleanupCategory,
1180
+ ) {
1181
+ const rows = await this.getCourseStorageRows(courseId);
1182
+
1183
+ const uniqueFiles = new Map<number, number>();
1184
+ for (const row of rows) {
1185
+ const rowCategory = String(row.storage_category || 'other_lesson_file');
1186
+ if (rowCategory !== category) continue;
1187
+
1188
+ const fileId = Number(row.file_id ?? 0);
1189
+ if (fileId <= 0) continue;
1190
+
1191
+ if (!uniqueFiles.has(fileId)) {
1192
+ uniqueFiles.set(fileId, Number(row.file_size ?? 0) || 0);
1193
+ }
1194
+ }
1195
+
1196
+ const fileIds = Array.from(uniqueFiles.keys());
1197
+ const totalBytes = Array.from(uniqueFiles.values()).reduce(
1198
+ (sum, size) => sum + size,
1199
+ 0,
1200
+ );
1201
+
1202
+ return {
1203
+ category,
1204
+ fileIds,
1205
+ fileCount: fileIds.length,
1206
+ totalBytes,
1207
+ };
1208
+ }
1209
+
1210
+ private async deleteCourseStorageCategoryLinks(
1211
+ courseId: number,
1212
+ category: CourseStorageCleanupCategory,
1213
+ ) {
1214
+ if (category === 'course_image') {
1215
+ await this.prisma.course_image.deleteMany({ where: { course_id: courseId } });
1216
+ return;
1217
+ }
1218
+
1219
+ if (category === 'course_file') {
1220
+ await this.prisma.course_file.deleteMany({ where: { course_id: courseId } });
1221
+ return;
1222
+ }
1223
+
1224
+ if (category === 'extracted_image') {
1225
+ await this.prisma.$executeRaw`
1226
+ DELETE FROM course_lesson_video_frame clvf
1227
+ USING course_lesson cl, course_module cm
1228
+ WHERE clvf.course_lesson_id = cl.id
1229
+ AND cl.course_module_id = cm.id
1230
+ AND cm.course_id = ${courseId}
1231
+ `;
1232
+ return;
1233
+ }
1234
+
1235
+ if (category === 'video_profile') {
1236
+ await this.prisma.$executeRaw`
1237
+ DELETE FROM course_lesson_file clf
1238
+ USING course_lesson cl, course_module cm
1239
+ WHERE clf.course_lesson_id = cl.id
1240
+ AND cl.course_module_id = cm.id
1241
+ AND cm.course_id = ${courseId}
1242
+ AND clf.type LIKE 'video_profile:%'
1243
+ `;
1244
+ return;
1245
+ }
1246
+
1247
+ if (category === 'other_lesson_file') {
1248
+ await this.prisma.$executeRaw`
1249
+ DELETE FROM course_lesson_file clf
1250
+ USING course_lesson cl, course_module cm
1251
+ WHERE clf.course_lesson_id = cl.id
1252
+ AND cl.course_module_id = cm.id
1253
+ AND cm.course_id = ${courseId}
1254
+ AND (
1255
+ clf.type IS NULL
1256
+ OR (
1257
+ clf.type <> 'video_original'
1258
+ AND clf.type <> 'lesson_audio'
1259
+ AND clf.type <> 'student_download'
1260
+ AND clf.type <> 'supplementary_material'
1261
+ AND clf.type NOT LIKE 'video_profile:%'
1262
+ )
1263
+ )
1264
+ `;
1265
+ return;
1266
+ }
1267
+
1268
+ await this.prisma.course_lesson_file.deleteMany({
1269
+ where: {
1270
+ course_lesson: { course_module: { course_id: courseId } },
1271
+ type: category,
1272
+ },
1273
+ });
1274
+ }
1275
+
1276
+ private async getCourseStorageRows(courseId: number) {
1277
+ return this.prisma.$queryRaw<CourseStorageRow[]>`
1278
+ SELECT files.file_id, files.file_size, files.storage_category
1279
+ FROM (
1280
+ SELECT ci.file_id, f.size AS file_size, 'course_image' AS storage_category
1281
+ FROM course_image ci
1282
+ INNER JOIN file f ON f.id = ci.file_id
1283
+ WHERE ci.course_id = ${courseId}
1284
+
1285
+ UNION ALL
1286
+
1287
+ SELECT cf.file_id, f.size AS file_size, 'course_file' AS storage_category
1288
+ FROM course_file cf
1289
+ INNER JOIN file f ON f.id = cf.file_id
1290
+ WHERE cf.course_id = ${courseId}
1291
+
1292
+ UNION ALL
1293
+
1294
+ SELECT clf.file_id,
1295
+ f.size AS file_size,
1296
+ CASE
1297
+ WHEN clf.type = 'video_original' THEN 'video_original'
1298
+ WHEN clf.type LIKE 'video_profile:%' THEN 'video_profile'
1299
+ WHEN clf.type = 'lesson_audio' THEN 'lesson_audio'
1300
+ WHEN clf.type = 'student_download' THEN 'student_download'
1301
+ WHEN clf.type = 'supplementary_material' THEN 'supplementary_material'
1302
+ ELSE 'other_lesson_file'
1303
+ END AS storage_category
1304
+ FROM course_lesson_file clf
1305
+ INNER JOIN file f ON f.id = clf.file_id
1306
+ INNER JOIN course_lesson cl ON cl.id = clf.course_lesson_id
1307
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1308
+ WHERE cm.course_id = ${courseId}
1309
+
1310
+ UNION ALL
1311
+
1312
+ SELECT clvf.file_id, f.size AS file_size, 'extracted_image' AS storage_category
1313
+ FROM course_lesson_video_frame clvf
1314
+ INNER JOIN file f ON f.id = clvf.file_id
1315
+ INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
1316
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1317
+ WHERE cm.course_id = ${courseId}
1318
+ ) AS files
1319
+ WHERE files.file_id IS NOT NULL
1320
+ `;
1321
+ }
1322
+
1323
+ private async getCourseDeletionImpact(courseId: number) {
749
1324
  const rows = await this.prisma.$queryRaw<
750
- Array<{ file_id: number | null }>
1325
+ Array<{
1326
+ file_id: number | null;
1327
+ file_size: number | null;
1328
+ source_type: string;
1329
+ }>
751
1330
  >`
752
- SELECT DISTINCT files.file_id
1331
+ SELECT DISTINCT files.file_id, files.file_size, files.source_type
753
1332
  FROM (
754
- SELECT ci.file_id
1333
+ SELECT ci.file_id, f.size AS file_size, 'course_image' AS source_type
755
1334
  FROM course_image ci
1335
+ INNER JOIN file f ON f.id = ci.file_id
756
1336
  WHERE ci.course_id = ${courseId}
757
1337
 
758
1338
  UNION ALL
759
1339
 
760
- SELECT cf.file_id
1340
+ SELECT cf.file_id, f.size AS file_size, 'course_file' AS source_type
761
1341
  FROM course_file cf
1342
+ INNER JOIN file f ON f.id = cf.file_id
762
1343
  WHERE cf.course_id = ${courseId}
763
1344
 
764
1345
  UNION ALL
765
1346
 
766
- SELECT clf.file_id
1347
+ SELECT clf.file_id, f.size AS file_size, 'course_lesson_file' AS source_type
767
1348
  FROM course_lesson_file clf
1349
+ INNER JOIN file f ON f.id = clf.file_id
768
1350
  INNER JOIN course_lesson cl ON cl.id = clf.course_lesson_id
769
1351
  INNER JOIN course_module cm ON cm.id = cl.course_module_id
770
1352
  WHERE cm.course_id = ${courseId}
771
1353
 
772
1354
  UNION ALL
773
1355
 
774
- SELECT clvf.file_id
1356
+ SELECT clvf.file_id, f.size AS file_size, 'course_lesson_video_frame' AS source_type
775
1357
  FROM course_lesson_video_frame clvf
1358
+ INNER JOIN file f ON f.id = clvf.file_id
776
1359
  INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
777
1360
  INNER JOIN course_module cm ON cm.id = cl.course_module_id
778
1361
  WHERE cm.course_id = ${courseId}
779
1362
 
780
1363
  UNION ALL
781
1364
 
782
- SELECT cgm.file_id
1365
+ SELECT cgm.file_id, f.size AS file_size, 'course_class_group_material' AS source_type
783
1366
  FROM course_class_group_material cgm
1367
+ INNER JOIN file f ON f.id = cgm.file_id
784
1368
  INNER JOIN course_class_group ccg ON ccg.id = cgm.course_class_group_id
785
1369
  WHERE ccg.course_id = ${courseId}
1370
+
1371
+ UNION ALL
1372
+
1373
+ SELECT cln.frame_file_id AS file_id, f.size AS file_size, 'course_lesson_note_frame' AS source_type
1374
+ FROM course_lesson_note cln
1375
+ INNER JOIN file f ON f.id = cln.frame_file_id
1376
+ INNER JOIN course_lesson cl ON cl.id = cln.course_lesson_id
1377
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1378
+ WHERE cm.course_id = ${courseId}
786
1379
  ) AS files
787
1380
  WHERE files.file_id IS NOT NULL
788
1381
  `;
789
1382
 
790
- return rows
791
- .map((row) => Number(row.file_id))
792
- .filter((id) => Number.isInteger(id) && id > 0);
1383
+ const uniqueRows = Array.from(
1384
+ rows.reduce((map, row) => {
1385
+ const fileId = Number(row.file_id ?? 0);
1386
+ if (fileId > 0 && !map.has(fileId)) {
1387
+ map.set(fileId, {
1388
+ fileId,
1389
+ size: Number(row.file_size ?? 0) || 0,
1390
+ sourceType: row.source_type,
1391
+ });
1392
+ }
1393
+ return map;
1394
+ }, new Map<number, { fileId: number; size: number; sourceType: string }>()).values(),
1395
+ );
1396
+
1397
+ const countsBySource = rows.reduce<Record<string, number>>((acc, row) => {
1398
+ const key = String(row.source_type || 'unknown');
1399
+ acc[key] = (acc[key] ?? 0) + 1;
1400
+ return acc;
1401
+ }, {});
1402
+
1403
+ return {
1404
+ deletionImpact: {
1405
+ fileIds: uniqueRows.map((row) => row.fileId),
1406
+ fileCount: uniqueRows.length,
1407
+ totalBytes: uniqueRows.reduce((sum, row) => sum + row.size, 0),
1408
+ countsBySource,
1409
+ },
1410
+ };
1411
+ }
1412
+
1413
+ private formatBytes(bytes: number) {
1414
+ if (!Number.isFinite(bytes) || bytes <= 0) {
1415
+ return '0 B';
1416
+ }
1417
+
1418
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1419
+ let value = bytes;
1420
+ let unitIndex = 0;
1421
+
1422
+ while (value >= 1024 && unitIndex < units.length - 1) {
1423
+ value /= 1024;
1424
+ unitIndex += 1;
1425
+ }
1426
+
1427
+ const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
1428
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
1429
+ }
1430
+
1431
+ private async getCourseRelatedFileIds(courseId: number) {
1432
+ const impact = await this.getCourseDeletionImpact(courseId);
1433
+
1434
+ return impact.deletionImpact.fileIds.filter(
1435
+ (id) => Number.isInteger(id) && id > 0,
1436
+ );
793
1437
  }
794
1438
 
795
1439
  private mapCourse(
@@ -1238,4 +1882,177 @@ export class CourseService {
1238
1882
  })),
1239
1883
  );
1240
1884
  }
1885
+
1886
+ async getCourseContentOverview(courseId: number) {
1887
+ const [
1888
+ moduleCount,
1889
+ lessons,
1890
+ transcriptionRows,
1891
+ xpMapRows,
1892
+ extractedImageCount,
1893
+ resourceFileCount,
1894
+ storageRows,
1895
+ ] = await Promise.all([
1896
+ this.prisma.course_module.count({ where: { course_id: courseId } }),
1897
+ this.prisma.course_lesson.findMany({
1898
+ where: { course_module: { course_id: courseId } },
1899
+ select: { id: true, type: true, content: true, published: true },
1900
+ }),
1901
+ this.prisma.course_lesson_transcription_segment.findMany({
1902
+ where: { course_lesson: { course_module: { course_id: courseId } } },
1903
+ select: { course_lesson_id: true },
1904
+ distinct: ['course_lesson_id'],
1905
+ }),
1906
+ this.prisma.lesson_xp_map.findMany({
1907
+ where: { course_lesson: { course_module: { course_id: courseId } } },
1908
+ select: { course_lesson_id: true },
1909
+ }),
1910
+ this.prisma.course_lesson_video_frame.count({
1911
+ where: { course_lesson: { course_module: { course_id: courseId } } },
1912
+ }),
1913
+ this.prisma.course_lesson_file.count({
1914
+ where: {
1915
+ course_lesson: { course_module: { course_id: courseId } },
1916
+ type: { in: ['student_download', 'supplementary_material'] },
1917
+ },
1918
+ }),
1919
+ this.getCourseStorageRows(courseId),
1920
+ ]);
1921
+
1922
+ const transcriptionLessonIds = new Set(
1923
+ transcriptionRows.map((r) => r.course_lesson_id),
1924
+ );
1925
+ const xpLessonIds = new Set(xpMapRows.map((r) => r.course_lesson_id));
1926
+
1927
+ const lessonsByType = { video: 0, questao: 0, post: 0, exercicio: 0 };
1928
+ let publishedLessonCount = 0;
1929
+ let videoWithTranscription = 0;
1930
+ let videoWithXp = 0;
1931
+
1932
+ const categoryOrder = [
1933
+ 'video_original',
1934
+ 'video_profile',
1935
+ 'lesson_audio',
1936
+ 'extracted_image',
1937
+ 'student_download',
1938
+ 'supplementary_material',
1939
+ 'course_image',
1940
+ 'course_file',
1941
+ 'other_lesson_file',
1942
+ ];
1943
+
1944
+ const uniqueStorageFiles = new Map<number, number>();
1945
+ const uniqueStorageCategoryEntries = new Set<string>();
1946
+ const storageCategoryMap = new Map<
1947
+ string,
1948
+ { key: string; fileCount: number; totalBytes: number }
1949
+ >();
1950
+
1951
+ for (const row of storageRows) {
1952
+ const fileId = Number(row.file_id ?? 0);
1953
+ if (fileId <= 0) continue;
1954
+
1955
+ const size = Number(row.file_size ?? 0) || 0;
1956
+ const category = String(row.storage_category || 'other_lesson_file');
1957
+
1958
+ if (!uniqueStorageFiles.has(fileId)) {
1959
+ uniqueStorageFiles.set(fileId, size);
1960
+ }
1961
+
1962
+ const categoryEntryKey = `${category}:${fileId}`;
1963
+ if (uniqueStorageCategoryEntries.has(categoryEntryKey)) {
1964
+ continue;
1965
+ }
1966
+
1967
+ uniqueStorageCategoryEntries.add(categoryEntryKey);
1968
+
1969
+ const current = storageCategoryMap.get(category) ?? {
1970
+ key: category,
1971
+ fileCount: 0,
1972
+ totalBytes: 0,
1973
+ };
1974
+
1975
+ current.fileCount += 1;
1976
+ current.totalBytes += size;
1977
+ storageCategoryMap.set(category, current);
1978
+ }
1979
+
1980
+ const storageCategories = Array.from(storageCategoryMap.values()).sort(
1981
+ (a, b) => {
1982
+ const orderA = categoryOrder.indexOf(a.key);
1983
+ const orderB = categoryOrder.indexOf(b.key);
1984
+
1985
+ if (orderA !== -1 || orderB !== -1) {
1986
+ return (orderA === -1 ? Number.MAX_SAFE_INTEGER : orderA) -
1987
+ (orderB === -1 ? Number.MAX_SAFE_INTEGER : orderB);
1988
+ }
1989
+
1990
+ return b.totalBytes - a.totalBytes;
1991
+ },
1992
+ );
1993
+
1994
+ for (const lesson of lessons) {
1995
+ if (lesson.published) publishedLessonCount++;
1996
+
1997
+ let sourceType: string | undefined;
1998
+ try {
1999
+ const parsed = lesson.content
2000
+ ? (JSON.parse(lesson.content as string) as Record<string, unknown>)
2001
+ : null;
2002
+ sourceType =
2003
+ typeof parsed?.sourceType === 'string'
2004
+ ? parsed.sourceType
2005
+ : undefined;
2006
+ } catch {
2007
+ // ignore malformed content
2008
+ }
2009
+
2010
+ let uiType: 'video' | 'questao' | 'post' | 'exercicio';
2011
+ if (
2012
+ sourceType === 'video' ||
2013
+ sourceType === 'questao' ||
2014
+ sourceType === 'post' ||
2015
+ sourceType === 'exercicio'
2016
+ ) {
2017
+ uiType = sourceType;
2018
+ } else if (lesson.type === 'video') {
2019
+ uiType = 'video';
2020
+ } else if (lesson.type === 'quiz') {
2021
+ uiType = 'questao';
2022
+ } else {
2023
+ uiType = 'post';
2024
+ }
2025
+
2026
+ lessonsByType[uiType]++;
2027
+
2028
+ if (uiType === 'video') {
2029
+ if (transcriptionLessonIds.has(lesson.id)) videoWithTranscription++;
2030
+ if (xpLessonIds.has(lesson.id)) videoWithXp++;
2031
+ }
2032
+ }
2033
+
2034
+ return {
2035
+ structure: {
2036
+ moduleCount,
2037
+ lessonCount: lessons.length,
2038
+ publishedLessonCount,
2039
+ lessonsByType,
2040
+ },
2041
+ videos: {
2042
+ lessonCount: lessonsByType.video,
2043
+ withTranscription: videoWithTranscription,
2044
+ withXp: videoWithXp,
2045
+ },
2046
+ media: { extractedImageCount },
2047
+ resources: { fileCount: resourceFileCount },
2048
+ storage: {
2049
+ totalBytes: Array.from(uniqueStorageFiles.values()).reduce(
2050
+ (sum, size) => sum + size,
2051
+ 0,
2052
+ ),
2053
+ totalFileCount: uniqueStorageFiles.size,
2054
+ categories: storageCategories,
2055
+ },
2056
+ };
2057
+ }
1241
2058
  }