@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
@@ -0,0 +1,116 @@
1
+ import { Role } from '@hed-hog/api';
2
+ import {
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ Get,
7
+ HttpCode,
8
+ HttpStatus,
9
+ Param,
10
+ ParseIntPipe,
11
+ Patch,
12
+ Post,
13
+ Query,
14
+ Request,
15
+ } from '@nestjs/common';
16
+ import { CreateLessonXpMapDto } from './dto/create-lesson-xp-map.dto';
17
+ import { CreateLessonXpSegmentDto } from './dto/create-lesson-xp-segment.dto';
18
+ import { RejectLessonXpMapDto } from './dto/review-lesson-xp-map.dto';
19
+ import { UpdateLessonXpMapDto } from './dto/update-lesson-xp-map.dto';
20
+ import { LessonXpAiCalculationService } from './lesson-xp-ai-calculation.service';
21
+ import { LessonXpMapService } from './lesson-xp-map.service';
22
+ import { LessonXpSegmentService } from './lesson-xp-segment.service';
23
+
24
+ @Role()
25
+ @Controller('lms/xp/lesson-maps')
26
+ export class LessonXpMapController {
27
+ constructor(
28
+ private readonly lessonXpMapService: LessonXpMapService,
29
+ private readonly lessonXpSegmentService: LessonXpSegmentService,
30
+ private readonly lessonXpAiCalculationService: LessonXpAiCalculationService,
31
+ ) {}
32
+
33
+ @Get()
34
+ list(
35
+ @Query('page') page?: string,
36
+ @Query('pageSize') pageSize?: string,
37
+ @Query('search') search?: string,
38
+ @Query('status') status?: string,
39
+ @Query('courseLessonId') courseLessonId?: string,
40
+ ) {
41
+ return this.lessonXpMapService.list({
42
+ page: Number(page) || 1,
43
+ pageSize: Number(pageSize) || 20,
44
+ search,
45
+ status,
46
+ courseLessonId: courseLessonId ? Number(courseLessonId) : undefined,
47
+ });
48
+ }
49
+
50
+ @Post()
51
+ create(@Body() dto: CreateLessonXpMapDto) {
52
+ return this.lessonXpMapService.create(dto);
53
+ }
54
+
55
+ @Get('lesson/:lessonId')
56
+ getByLesson(@Param('lessonId', ParseIntPipe) lessonId: number) {
57
+ return this.lessonXpMapService.getByLessonId(lessonId);
58
+ }
59
+
60
+ @Get('course/:courseId/overview')
61
+ getCourseOverview(@Param('courseId', ParseIntPipe) courseId: number) {
62
+ return this.lessonXpMapService.getCourseOverview(courseId);
63
+ }
64
+
65
+ @Post('lesson/:lessonId/recalculate')
66
+ recalculate(@Param('lessonId', ParseIntPipe) lessonId: number, @Request() req: any) {
67
+ return this.lessonXpAiCalculationService.triggerCalculation(lessonId, req.user?.id ?? 0);
68
+ }
69
+
70
+ @Get(':id')
71
+ getById(@Param('id', ParseIntPipe) id: number) {
72
+ return this.lessonXpMapService.getById(id);
73
+ }
74
+
75
+ @Patch(':id')
76
+ update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateLessonXpMapDto) {
77
+ return this.lessonXpMapService.update(id, dto);
78
+ }
79
+
80
+ @Delete(':id')
81
+ @HttpCode(HttpStatus.NO_CONTENT)
82
+ remove(@Param('id', ParseIntPipe) id: number) {
83
+ return this.lessonXpMapService.delete(id);
84
+ }
85
+
86
+ @Post(':id/approve')
87
+ approve(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
88
+ const reviewerPersonId: number = req.user?.personId ?? req.user?.id ?? 0;
89
+ return this.lessonXpMapService.approve(id, reviewerPersonId);
90
+ }
91
+
92
+ @Post(':id/reject')
93
+ reject(
94
+ @Param('id', ParseIntPipe) id: number,
95
+ @Body() dto: RejectLessonXpMapDto,
96
+ @Request() req: any,
97
+ ) {
98
+ const reviewerPersonId: number = req.user?.personId ?? req.user?.id ?? 0;
99
+ return this.lessonXpMapService.reject(id, reviewerPersonId, dto.reviewNotes);
100
+ }
101
+
102
+ // Segment sub-resource
103
+ @Get(':id/segments')
104
+ listSegments(
105
+ @Param('id', ParseIntPipe) id: number,
106
+ @Query('page') page?: string,
107
+ @Query('pageSize') pageSize?: string,
108
+ ) {
109
+ return this.lessonXpSegmentService.listByMap(id, { page: Number(page) || 1, pageSize: Number(pageSize) || 50 });
110
+ }
111
+
112
+ @Post(':id/segments')
113
+ createSegment(@Param('id', ParseIntPipe) id: number, @Body() dto: CreateLessonXpSegmentDto) {
114
+ return this.lessonXpSegmentService.create(id, dto);
115
+ }
116
+ }
@@ -0,0 +1,21 @@
1
+ import { PrismaModule } from '@hed-hog/api-prisma';
2
+ import { CoreModule } from '@hed-hog/core';
3
+ import { QueueModule } from '@hed-hog/queue';
4
+ import { forwardRef, Module } from '@nestjs/common';
5
+ import { LessonXpAiCalculationService } from './lesson-xp-ai-calculation.service';
6
+ import { LessonXpMapController } from './lesson-xp-map.controller';
7
+ import { LessonXpMapService } from './lesson-xp-map.service';
8
+ import { LessonXpSegmentController } from './lesson-xp-segment.controller';
9
+ import { LessonXpSegmentService } from './lesson-xp-segment.service';
10
+
11
+ @Module({
12
+ imports: [
13
+ forwardRef(() => PrismaModule),
14
+ forwardRef(() => CoreModule),
15
+ forwardRef(() => QueueModule),
16
+ ],
17
+ controllers: [LessonXpMapController, LessonXpSegmentController],
18
+ providers: [LessonXpMapService, LessonXpSegmentService, LessonXpAiCalculationService],
19
+ exports: [LessonXpMapService, LessonXpSegmentService, LessonXpAiCalculationService],
20
+ })
21
+ export class LessonXpMapModule {}
@@ -0,0 +1,442 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable, NotFoundException } from '@nestjs/common';
3
+ import { CreateLessonXpMapDto } from './dto/create-lesson-xp-map.dto';
4
+ import { UpdateLessonXpMapDto } from './dto/update-lesson-xp-map.dto';
5
+
6
+ @Injectable()
7
+ export class LessonXpMapService {
8
+ constructor(private readonly prisma: PrismaService) {}
9
+
10
+ async list(params: {
11
+ page?: number;
12
+ pageSize?: number;
13
+ search?: string;
14
+ status?: string;
15
+ courseLessonId?: number;
16
+ }) {
17
+ const page = Math.max(Number(params.page) || 1, 1);
18
+ const pageSize = Math.max(Number(params.pageSize) || 20, 1);
19
+ const offset = (page - 1) * pageSize;
20
+
21
+ const values: unknown[] = [];
22
+ const where: string[] = ['1 = 1'];
23
+
24
+ if (params.search?.trim()) {
25
+ const p = this.param(values, `%${params.search.trim()}%`);
26
+ where.push(`cl.title ILIKE ${p}`);
27
+ }
28
+ if (params.status?.trim()) {
29
+ where.push(`m.status::text = ${this.param(values, params.status.trim())}`);
30
+ }
31
+ if (params.courseLessonId) {
32
+ where.push(`m.course_lesson_id = ${this.param(values, params.courseLessonId)}`);
33
+ }
34
+
35
+ const whereClause = where.join(' AND ');
36
+
37
+ const totalRow = await this.querySingle<{ total: string }>(
38
+ `SELECT COUNT(*)::text AS total
39
+ FROM lesson_xp_map m
40
+ JOIN course_lesson cl ON cl.id = m.course_lesson_id
41
+ WHERE ${whereClause}`,
42
+ values,
43
+ );
44
+
45
+ const q = [...values];
46
+ const rows = await this.queryRows<any>(
47
+ `SELECT
48
+ m.id,
49
+ m.course_lesson_id AS "courseLessonId",
50
+ cl.title AS "lessonTitle",
51
+ m.version,
52
+ m.total_xp AS "totalXp",
53
+ m.status::text AS status,
54
+ m.generated_at::text AS "generatedAt",
55
+ m.reviewed_at::text AS "reviewedAt",
56
+ m.reviewed_by_person_id AS "reviewedByPersonId",
57
+ m.review_notes AS "reviewNotes",
58
+ m.ai_model_version AS "aiModelVersion",
59
+ m.processing_error AS "processingError",
60
+ m.created_at::text AS "createdAt",
61
+ m.updated_at::text AS "updatedAt",
62
+ (SELECT COUNT(*) FROM lesson_xp_segment s WHERE s.lesson_xp_map_id = m.id)::int AS "segmentCount"
63
+ FROM lesson_xp_map m
64
+ JOIN course_lesson cl ON cl.id = m.course_lesson_id
65
+ WHERE ${whereClause}
66
+ ORDER BY m.updated_at DESC
67
+ LIMIT ${this.param(q, pageSize)} OFFSET ${this.param(q, offset)}`,
68
+ q,
69
+ );
70
+
71
+ const total = Number(totalRow?.total ?? 0);
72
+ return { data: rows, total, page, pageSize, lastPage: Math.ceil(total / pageSize) };
73
+ }
74
+
75
+ async getById(id: number) {
76
+ const row = await this.querySingle<any>(
77
+ `SELECT
78
+ m.id,
79
+ m.course_lesson_id AS "courseLessonId",
80
+ cl.title AS "lessonTitle",
81
+ m.version,
82
+ m.total_xp AS "totalXp",
83
+ m.status::text AS status,
84
+ m.generated_at::text AS "generatedAt",
85
+ m.reviewed_at::text AS "reviewedAt",
86
+ m.reviewed_by_person_id AS "reviewedByPersonId",
87
+ m.review_notes AS "reviewNotes",
88
+ m.ai_model_version AS "aiModelVersion",
89
+ m.processing_error AS "processingError",
90
+ m.created_at::text AS "createdAt",
91
+ m.updated_at::text AS "updatedAt"
92
+ FROM lesson_xp_map m
93
+ JOIN course_lesson cl ON cl.id = m.course_lesson_id
94
+ WHERE m.id = $1`,
95
+ [id],
96
+ );
97
+ if (!row) throw new NotFoundException('Lesson XP map not found');
98
+
99
+ const segments = await this.queryRows<any>(
100
+ `SELECT
101
+ s.id,
102
+ s.start_seconds::float AS "startSeconds",
103
+ s.end_seconds::float AS "endSeconds",
104
+ s.xp_value AS "xpValue",
105
+ s.difficulty::text AS difficulty,
106
+ s.should_grant_xp AS "shouldGrantXp",
107
+ s.ai_summary AS "aiSummary",
108
+ s.ai_confidence::float AS "aiConfidence",
109
+ s.sort_order AS "sortOrder",
110
+ COALESCE(
111
+ (SELECT json_agg(json_build_object('xpAreaId', sa.xp_area_id, 'weightPercent', sa.weight_percent::float))
112
+ FROM lesson_xp_segment_area sa WHERE sa.lesson_xp_segment_id = s.id),
113
+ '[]'
114
+ ) AS areas,
115
+ COALESCE(
116
+ (SELECT json_agg(json_build_object('xpSkillId', ss.xp_skill_id, 'weightPercent', ss.weight_percent::float))
117
+ FROM lesson_xp_segment_skill ss WHERE ss.lesson_xp_segment_id = s.id),
118
+ '[]'
119
+ ) AS skills,
120
+ COALESCE(
121
+ (SELECT json_agg(
122
+ json_build_object(
123
+ 'xpLearningTypeId', sl.xp_learning_type_id,
124
+ 'weightPercent', sl.weight_percent::float,
125
+ 'slug', lt.slug,
126
+ 'name', COALESCE(lt_pt.name, lt_en.name, lt.slug),
127
+ 'multiplier', lt.multiplier::float
128
+ )
129
+ )
130
+ FROM lesson_xp_segment_learning_type sl
131
+ JOIN xp_learning_type lt ON lt.id = sl.xp_learning_type_id
132
+ LEFT JOIN xp_learning_type_locale lt_pt
133
+ ON lt_pt.xp_learning_type_id = lt.id
134
+ AND COALESCE(
135
+ (SELECT code FROM locale WHERE id = NULLIF(to_jsonb(lt_pt)->>'locale_id', '')::int),
136
+ to_jsonb(lt_pt)->>'locale'
137
+ ) = 'pt'
138
+ LEFT JOIN xp_learning_type_locale lt_en
139
+ ON lt_en.xp_learning_type_id = lt.id
140
+ AND COALESCE(
141
+ (SELECT code FROM locale WHERE id = NULLIF(to_jsonb(lt_en)->>'locale_id', '')::int),
142
+ to_jsonb(lt_en)->>'locale'
143
+ ) = 'en'
144
+ WHERE sl.lesson_xp_segment_id = s.id),
145
+ '[]'
146
+ ) AS "learningTypes"
147
+ FROM lesson_xp_segment s
148
+ WHERE s.lesson_xp_map_id = $1
149
+ ORDER BY s.sort_order ASC, s.start_seconds ASC`,
150
+ [id],
151
+ );
152
+
153
+ return { ...row, segments };
154
+ }
155
+
156
+ async getByLessonId(lessonId: number) {
157
+ const row = await this.querySingle<{ id: number }>(
158
+ `SELECT id FROM lesson_xp_map WHERE course_lesson_id = $1`,
159
+ [lessonId],
160
+ );
161
+ if (!row) return null;
162
+ return this.getById(row.id);
163
+ }
164
+
165
+ async getCourseOverview(courseId: number) {
166
+ const lessons = await this.queryRows<{
167
+ courseLessonId: number;
168
+ lessonCode: string | null;
169
+ lessonTitle: string;
170
+ totalXp: number;
171
+ status: string;
172
+ segmentCount: number;
173
+ hasMap: boolean;
174
+ }>(
175
+ `SELECT
176
+ l.id AS "courseLessonId",
177
+ l.id::text AS "lessonCode",
178
+ l.title AS "lessonTitle",
179
+ COALESCE(lm.total_xp, 0)::int AS "totalXp",
180
+ COALESCE(lm.status::text, 'pending') AS status,
181
+ COALESCE(
182
+ (SELECT COUNT(*)::int
183
+ FROM lesson_xp_segment s
184
+ WHERE s.lesson_xp_map_id = lm.id
185
+ AND s.should_grant_xp = true),
186
+ 0
187
+ )::int AS "segmentCount",
188
+ (lm.id IS NOT NULL) AS "hasMap"
189
+ FROM course_lesson l
190
+ JOIN course_module cm ON cm.id = l.course_module_id
191
+ LEFT JOIN lesson_xp_map lm ON lm.course_lesson_id = l.id
192
+ WHERE cm.course_id = $1
193
+ ORDER BY l.id ASC`,
194
+ [courseId],
195
+ );
196
+
197
+ const areas = await this.queryRows<{
198
+ xpAreaId: number;
199
+ slug: string;
200
+ name: string;
201
+ xp: number;
202
+ lessonCount: number;
203
+ segmentCount: number;
204
+ }>(
205
+ `SELECT
206
+ a.id AS "xpAreaId",
207
+ a.slug,
208
+ COALESCE(a_pt.name, a_en.name, a.slug) AS name,
209
+ SUM(ROUND(s.xp_value * (sa.weight_percent::numeric / 100)))::int AS xp,
210
+ COUNT(DISTINCT lm.course_lesson_id)::int AS "lessonCount",
211
+ COUNT(*)::int AS "segmentCount"
212
+ FROM lesson_xp_map lm
213
+ JOIN course_lesson l ON l.id = lm.course_lesson_id
214
+ JOIN course_module cm ON cm.id = l.course_module_id
215
+ JOIN lesson_xp_segment s
216
+ ON s.lesson_xp_map_id = lm.id
217
+ AND s.should_grant_xp = true
218
+ JOIN lesson_xp_segment_area sa ON sa.lesson_xp_segment_id = s.id
219
+ JOIN xp_area a ON a.id = sa.xp_area_id
220
+ LEFT JOIN xp_area_locale a_pt
221
+ ON a_pt.xp_area_id = a.id
222
+ AND a_pt.locale_id = (SELECT id FROM locale WHERE code = 'pt')
223
+ LEFT JOIN xp_area_locale a_en
224
+ ON a_en.xp_area_id = a.id
225
+ AND a_en.locale_id = (SELECT id FROM locale WHERE code = 'en')
226
+ WHERE cm.course_id = $1
227
+ GROUP BY a.id, a.slug, a_pt.name, a_en.name
228
+ ORDER BY xp DESC, name ASC`,
229
+ [courseId],
230
+ );
231
+
232
+ const skills = await this.queryRows<{
233
+ xpSkillId: number;
234
+ slug: string;
235
+ name: string;
236
+ xp: number;
237
+ lessonCount: number;
238
+ segmentCount: number;
239
+ }>(
240
+ `SELECT
241
+ sk.id AS "xpSkillId",
242
+ sk.slug,
243
+ COALESCE(sk_pt.name, sk_en.name, sk.slug) AS name,
244
+ SUM(ROUND(s.xp_value * (ss.weight_percent::numeric / 100)))::int AS xp,
245
+ COUNT(DISTINCT lm.course_lesson_id)::int AS "lessonCount",
246
+ COUNT(*)::int AS "segmentCount"
247
+ FROM lesson_xp_map lm
248
+ JOIN course_lesson l ON l.id = lm.course_lesson_id
249
+ JOIN course_module cm ON cm.id = l.course_module_id
250
+ JOIN lesson_xp_segment s
251
+ ON s.lesson_xp_map_id = lm.id
252
+ AND s.should_grant_xp = true
253
+ JOIN lesson_xp_segment_skill ss ON ss.lesson_xp_segment_id = s.id
254
+ JOIN xp_skill sk ON sk.id = ss.xp_skill_id
255
+ LEFT JOIN xp_skill_locale sk_pt
256
+ ON sk_pt.xp_skill_id = sk.id
257
+ AND sk_pt.locale_id = (SELECT id FROM locale WHERE code = 'pt')
258
+ LEFT JOIN xp_skill_locale sk_en
259
+ ON sk_en.xp_skill_id = sk.id
260
+ AND sk_en.locale_id = (SELECT id FROM locale WHERE code = 'en')
261
+ WHERE cm.course_id = $1
262
+ GROUP BY sk.id, sk.slug, sk_pt.name, sk_en.name
263
+ ORDER BY xp DESC, name ASC`,
264
+ [courseId],
265
+ );
266
+
267
+ const learningTypes = await this.queryRows<{
268
+ xpLearningTypeId: number;
269
+ slug: string;
270
+ name: string;
271
+ multiplier: number;
272
+ xp: number;
273
+ lessonCount: number;
274
+ segmentCount: number;
275
+ }>(
276
+ `SELECT
277
+ lt.id AS "xpLearningTypeId",
278
+ lt.slug,
279
+ COALESCE(lt_pt.name, lt_en.name, lt.slug) AS name,
280
+ lt.multiplier::float AS multiplier,
281
+ SUM(ROUND(s.xp_value * (sl.weight_percent::numeric / 100)))::int AS xp,
282
+ COUNT(DISTINCT lm.course_lesson_id)::int AS "lessonCount",
283
+ COUNT(*)::int AS "segmentCount"
284
+ FROM lesson_xp_map lm
285
+ JOIN course_lesson l ON l.id = lm.course_lesson_id
286
+ JOIN course_module cm ON cm.id = l.course_module_id
287
+ JOIN lesson_xp_segment s
288
+ ON s.lesson_xp_map_id = lm.id
289
+ AND s.should_grant_xp = true
290
+ JOIN lesson_xp_segment_learning_type sl ON sl.lesson_xp_segment_id = s.id
291
+ JOIN xp_learning_type lt ON lt.id = sl.xp_learning_type_id
292
+ LEFT JOIN xp_learning_type_locale lt_pt
293
+ ON lt_pt.xp_learning_type_id = lt.id
294
+ AND COALESCE(
295
+ (SELECT code FROM locale WHERE id = NULLIF(to_jsonb(lt_pt)->>'locale_id', '')::int),
296
+ to_jsonb(lt_pt)->>'locale'
297
+ ) = 'pt'
298
+ LEFT JOIN xp_learning_type_locale lt_en
299
+ ON lt_en.xp_learning_type_id = lt.id
300
+ AND COALESCE(
301
+ (SELECT code FROM locale WHERE id = NULLIF(to_jsonb(lt_en)->>'locale_id', '')::int),
302
+ to_jsonb(lt_en)->>'locale'
303
+ ) = 'en'
304
+ WHERE cm.course_id = $1
305
+ GROUP BY lt.id, lt.slug, lt.multiplier, lt_pt.name, lt_en.name
306
+ ORDER BY xp DESC, name ASC`,
307
+ [courseId],
308
+ );
309
+
310
+ const totalXp = lessons.reduce((sum, lesson) => sum + Number(lesson.totalXp || 0), 0);
311
+ const mappedLessons = lessons.filter((lesson) => lesson.hasMap).length;
312
+ const totalSegments = lessons.reduce((sum, lesson) => sum + Number(lesson.segmentCount || 0), 0);
313
+
314
+ return {
315
+ summary: {
316
+ totalLessons: lessons.length,
317
+ mappedLessons,
318
+ unmappedLessons: Math.max(lessons.length - mappedLessons, 0),
319
+ totalXp,
320
+ totalSegments,
321
+ averageXpPerMappedLesson: mappedLessons > 0 ? totalXp / mappedLessons : 0,
322
+ },
323
+ lessons,
324
+ areas: this.withShare(areas, totalXp),
325
+ skills: this.withShare(skills, totalXp),
326
+ learningTypes: this.withShare(learningTypes, totalXp),
327
+ };
328
+ }
329
+
330
+ async create(dto: CreateLessonXpMapDto) {
331
+ const existing = await this.querySingle<{ id: number }>(
332
+ `SELECT id FROM lesson_xp_map WHERE course_lesson_id = $1`,
333
+ [dto.courseLessonId],
334
+ );
335
+ if (existing) throw new NotFoundException(`A lesson XP map already exists for lesson ${dto.courseLessonId}. Use PATCH to update it.`);
336
+
337
+ const created = await this.querySingle<{ id: number }>(
338
+ `INSERT INTO lesson_xp_map (course_lesson_id, version, total_xp, status, ai_model_version, created_at, updated_at)
339
+ VALUES ($1, $2, 0, 'pending'::lesson_xp_map_status_d4e5f6a7b8_enum, $3, NOW(), NOW())
340
+ RETURNING id`,
341
+ [dto.courseLessonId, dto.version ?? 1, dto.aiModelVersion ?? null],
342
+ );
343
+ return this.getById(created!.id);
344
+ }
345
+
346
+ async update(id: number, dto: UpdateLessonXpMapDto) {
347
+ await this.ensureExists(id);
348
+
349
+ const updates: string[] = [];
350
+ const values: unknown[] = [];
351
+
352
+ if (dto.status !== undefined) updates.push(`status = ${this.param(values, dto.status)}::lesson_xp_map_status_d4e5f6a7b8_enum`);
353
+ if (dto.reviewNotes !== undefined) updates.push(`review_notes = ${this.param(values, dto.reviewNotes)}`);
354
+ if (dto.aiModelVersion !== undefined) updates.push(`ai_model_version = ${this.param(values, dto.aiModelVersion)}`);
355
+ if (dto.processingError !== undefined) updates.push(`processing_error = ${this.param(values, dto.processingError)}`);
356
+ if (dto.totalXp !== undefined) updates.push(`total_xp = ${this.param(values, dto.totalXp)}`);
357
+
358
+ if (updates.length > 0) {
359
+ values.push(id);
360
+ await this.prisma.$executeRawUnsafe(
361
+ `UPDATE lesson_xp_map SET ${updates.join(', ')}, updated_at = NOW() WHERE id = $${values.length}`,
362
+ ...values,
363
+ );
364
+ }
365
+
366
+ return this.getById(id);
367
+ }
368
+
369
+ async delete(id: number) {
370
+ await this.ensureExists(id);
371
+ await this.prisma.$executeRawUnsafe(`DELETE FROM lesson_xp_map WHERE id = $1`, id);
372
+ }
373
+
374
+ async approve(id: number, reviewerPersonId: number) {
375
+ await this.ensureExists(id);
376
+ await this.prisma.$executeRawUnsafe(
377
+ `UPDATE lesson_xp_map
378
+ SET status = 'approved'::lesson_xp_map_status_d4e5f6a7b8_enum,
379
+ reviewed_at = NOW(),
380
+ reviewed_by_person_id = $1,
381
+ updated_at = NOW()
382
+ WHERE id = $2`,
383
+ reviewerPersonId, id,
384
+ );
385
+ return this.getById(id);
386
+ }
387
+
388
+ async reject(id: number, reviewerPersonId: number, notes?: string) {
389
+ await this.ensureExists(id);
390
+ await this.prisma.$executeRawUnsafe(
391
+ `UPDATE lesson_xp_map
392
+ SET status = 'rejected'::lesson_xp_map_status_d4e5f6a7b8_enum,
393
+ reviewed_at = NOW(),
394
+ reviewed_by_person_id = $1,
395
+ review_notes = $2,
396
+ updated_at = NOW()
397
+ WHERE id = $3`,
398
+ reviewerPersonId, notes ?? null, id,
399
+ );
400
+ return this.getById(id);
401
+ }
402
+
403
+ async recalculateTotalXp(mapId: number) {
404
+ await this.prisma.$executeRawUnsafe(
405
+ `UPDATE lesson_xp_map
406
+ SET total_xp = COALESCE(
407
+ (SELECT SUM(xp_value) FROM lesson_xp_segment
408
+ WHERE lesson_xp_map_id = $1 AND should_grant_xp = true),
409
+ 0
410
+ ),
411
+ updated_at = NOW()
412
+ WHERE id = $1`,
413
+ mapId,
414
+ );
415
+ }
416
+
417
+ private async ensureExists(id: number) {
418
+ const row = await this.querySingle<{ id: number }>(`SELECT id FROM lesson_xp_map WHERE id = $1`, [id]);
419
+ if (!row) throw new NotFoundException('Lesson XP map not found');
420
+ }
421
+
422
+ private param(values: unknown[], value: unknown) {
423
+ values.push(value);
424
+ return `$${values.length}`;
425
+ }
426
+
427
+ private withShare<T extends { xp: number }>(items: T[], totalXp: number) {
428
+ return items.map((item) => ({
429
+ ...item,
430
+ sharePercent: totalXp > 0 ? Number(((item.xp / totalXp) * 100).toFixed(2)) : 0,
431
+ }));
432
+ }
433
+
434
+ private async queryRows<T>(query: string, values: unknown[] = []) {
435
+ return this.prisma.$queryRawUnsafe<T[]>(query, ...values);
436
+ }
437
+
438
+ private async querySingle<T>(query: string, values: unknown[] = []) {
439
+ const rows = await this.prisma.$queryRawUnsafe<T[]>(query, ...values);
440
+ return rows[0] ?? null;
441
+ }
442
+ }
@@ -0,0 +1,36 @@
1
+ import { Role } from '@hed-hog/api';
2
+ import {
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ Get,
7
+ HttpCode,
8
+ HttpStatus,
9
+ Param,
10
+ ParseIntPipe,
11
+ Patch,
12
+ } from '@nestjs/common';
13
+ import { UpdateLessonXpSegmentDto } from './dto/update-lesson-xp-segment.dto';
14
+ import { LessonXpSegmentService } from './lesson-xp-segment.service';
15
+
16
+ @Role()
17
+ @Controller('lms/xp/segments')
18
+ export class LessonXpSegmentController {
19
+ constructor(private readonly lessonXpSegmentService: LessonXpSegmentService) {}
20
+
21
+ @Get(':id')
22
+ getById(@Param('id', ParseIntPipe) id: number) {
23
+ return this.lessonXpSegmentService.getById(id);
24
+ }
25
+
26
+ @Patch(':id')
27
+ update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateLessonXpSegmentDto) {
28
+ return this.lessonXpSegmentService.update(id, dto);
29
+ }
30
+
31
+ @Delete(':id')
32
+ @HttpCode(HttpStatus.NO_CONTENT)
33
+ remove(@Param('id', ParseIntPipe) id: number) {
34
+ return this.lessonXpSegmentService.delete(id);
35
+ }
36
+ }