@hed-hog/lms 0.0.361 → 0.0.365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +66 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +91 -0
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
  6. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  7. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
  8. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
  9. package/dist/class-group/class-group.controller.d.ts +16 -16
  10. package/dist/class-group/class-group.service.d.ts +12 -12
  11. package/dist/course/course-audio-transcription.service.d.ts +3 -2
  12. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  13. package/dist/course/course-audio-transcription.service.js +49 -8
  14. package/dist/course/course-audio-transcription.service.js.map +1 -1
  15. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  16. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  17. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  18. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  19. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  20. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  21. package/dist/course/course-export-scorm12.service.js +628 -0
  22. package/dist/course/course-export-scorm12.service.js.map +1 -0
  23. package/dist/course/course-export.service.d.ts +84 -0
  24. package/dist/course/course-export.service.d.ts.map +1 -0
  25. package/dist/course/course-export.service.js +237 -0
  26. package/dist/course/course-export.service.js.map +1 -0
  27. package/dist/course/course-lesson.controller.d.ts +4 -0
  28. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  29. package/dist/course/course-lesson.controller.js +10 -0
  30. package/dist/course/course-lesson.controller.js.map +1 -1
  31. package/dist/course/course-structure.controller.d.ts +24 -9
  32. package/dist/course/course-structure.controller.d.ts.map +1 -1
  33. package/dist/course/course-structure.controller.js +30 -3
  34. package/dist/course/course-structure.controller.js.map +1 -1
  35. package/dist/course/course-structure.service.d.ts +25 -3
  36. package/dist/course/course-structure.service.d.ts.map +1 -1
  37. package/dist/course/course-structure.service.js +234 -24
  38. package/dist/course/course-structure.service.js.map +1 -1
  39. package/dist/course/course-video-conversion.service.d.ts +8 -0
  40. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  41. package/dist/course/course-video-conversion.service.js +87 -51
  42. package/dist/course/course-video-conversion.service.js.map +1 -1
  43. package/dist/course/course-video-hls.service.d.ts +57 -0
  44. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  45. package/dist/course/course-video-hls.service.js +767 -0
  46. package/dist/course/course-video-hls.service.js.map +1 -0
  47. package/dist/course/course.controller.d.ts +115 -11
  48. package/dist/course/course.controller.d.ts.map +1 -1
  49. package/dist/course/course.controller.js +66 -28
  50. package/dist/course/course.controller.js.map +1 -1
  51. package/dist/course/course.mcp-tools.js +1 -1
  52. package/dist/course/course.mcp-tools.js.map +1 -1
  53. package/dist/course/course.module.d.ts.map +1 -1
  54. package/dist/course/course.module.js +13 -0
  55. package/dist/course/course.module.js.map +1 -1
  56. package/dist/course/course.service.d.ts +112 -11
  57. package/dist/course/course.service.d.ts.map +1 -1
  58. package/dist/course/course.service.js +682 -72
  59. package/dist/course/course.service.js.map +1 -1
  60. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  61. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  62. package/dist/course/dto/cleanup-course-storage.dto.js +34 -0
  63. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  64. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  65. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  66. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  67. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  68. package/dist/course/dto/create-course-bulk-job.dto.d.ts +5 -0
  69. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  70. package/dist/course/dto/create-course-bulk-job.dto.js +26 -0
  71. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  72. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  73. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  74. package/dist/course/dto/create-course-export.dto.js +71 -0
  75. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  76. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  77. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  78. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  79. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  80. package/dist/course/lms-bulk-upload-automation.service.d.ts +54 -0
  81. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  82. package/dist/course/lms-bulk-upload-automation.service.js +537 -0
  83. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  84. package/dist/course/lms-bulk-upload-infra.service.d.ts +32 -0
  85. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  86. package/dist/course/lms-bulk-upload-infra.service.js +301 -0
  87. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  88. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  89. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  90. package/dist/course/lms-bulk-upload.constants.js +7 -0
  91. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  92. package/dist/course/lms-bulk-upload.controller.d.ts +144 -1
  93. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  94. package/dist/course/lms-bulk-upload.controller.js +114 -4
  95. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  96. package/dist/course/lms-bulk-upload.service.d.ts +153 -3
  97. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  98. package/dist/course/lms-bulk-upload.service.js +659 -21
  99. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  100. package/dist/course/lms-setting.controller.d.ts +6 -2
  101. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  102. package/dist/course/lms-setting.controller.js +25 -8
  103. package/dist/course/lms-setting.controller.js.map +1 -1
  104. package/dist/course/scorm12-schemas.d.ts +4 -0
  105. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  106. package/dist/course/scorm12-schemas.js +9 -0
  107. package/dist/course/scorm12-schemas.js.map +1 -0
  108. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  109. package/dist/enterprise/enterprise.service.d.ts +20 -20
  110. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  111. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  112. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  113. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  114. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  115. package/dist/enterprise/training/training-student.service.d.ts +52 -1
  116. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  117. package/dist/enterprise/training/training-student.service.js +217 -4
  118. package/dist/enterprise/training/training-student.service.js.map +1 -1
  119. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  120. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  121. package/dist/evaluation/evaluation.service.d.ts +26 -8
  122. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  123. package/dist/evaluation/evaluation.service.js +125 -0
  124. package/dist/evaluation/evaluation.service.js.map +1 -1
  125. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  126. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  127. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  128. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  129. package/dist/exam/exam.module.d.ts.map +1 -1
  130. package/dist/exam/exam.module.js +2 -1
  131. package/dist/exam/exam.module.js.map +1 -1
  132. package/dist/exam/exam.service.d.ts +21 -0
  133. package/dist/exam/exam.service.d.ts.map +1 -1
  134. package/dist/exam/exam.service.js +80 -0
  135. package/dist/exam/exam.service.js.map +1 -1
  136. package/dist/exam/question.controller.d.ts +27 -0
  137. package/dist/exam/question.controller.d.ts.map +1 -0
  138. package/dist/exam/question.controller.js +53 -0
  139. package/dist/exam/question.controller.js.map +1 -0
  140. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  141. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  142. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  143. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  144. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  145. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  146. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  147. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  148. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  149. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  150. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  151. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  152. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  153. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  154. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  155. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  156. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  157. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  158. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  159. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  160. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +30 -0
  161. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  162. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +440 -0
  163. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  164. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  165. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  166. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  167. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  168. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  169. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  170. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  171. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  172. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  173. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  174. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  175. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  176. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  177. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  178. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  179. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  180. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  181. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  182. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  183. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  184. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  185. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  186. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  187. package/dist/lms-commerce-access.subscriber.js +74 -0
  188. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  189. package/dist/lms.module.d.ts.map +1 -1
  190. package/dist/lms.module.js +21 -5
  191. package/dist/lms.module.js.map +1 -1
  192. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  193. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  194. package/dist/platforma/dto/update-profile.dto.js +87 -0
  195. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  196. package/dist/platforma/platforma-video.service.d.ts +39 -0
  197. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  198. package/dist/platforma/platforma-video.service.js +301 -0
  199. package/dist/platforma/platforma-video.service.js.map +1 -0
  200. package/dist/platforma/platforma.controller.d.ts +182 -1
  201. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  202. package/dist/platforma/platforma.controller.js +243 -2
  203. package/dist/platforma/platforma.controller.js.map +1 -1
  204. package/dist/platforma/platforma.service.d.ts +27 -0
  205. package/dist/platforma/platforma.service.d.ts.map +1 -0
  206. package/dist/platforma/platforma.service.js +274 -0
  207. package/dist/platforma/platforma.service.js.map +1 -0
  208. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  209. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  210. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  211. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  212. package/dist/student-xp/student-xp.controller.d.ts +56 -0
  213. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  214. package/dist/student-xp/student-xp.controller.js +138 -0
  215. package/dist/student-xp/student-xp.controller.js.map +1 -0
  216. package/dist/student-xp/student-xp.module.d.ts +3 -0
  217. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  218. package/dist/student-xp/student-xp.module.js +25 -0
  219. package/dist/student-xp/student-xp.module.js.map +1 -0
  220. package/dist/student-xp/student-xp.service.d.ts +81 -0
  221. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  222. package/dist/student-xp/student-xp.service.js +247 -0
  223. package/dist/student-xp/student-xp.service.js.map +1 -0
  224. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  225. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  226. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  227. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  228. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  229. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  230. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  231. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  232. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  233. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  234. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  235. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  236. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  237. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  238. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  239. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  240. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  241. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  242. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  243. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  244. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  245. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  246. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  247. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  248. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  249. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  250. package/dist/xp-catalog/xp-area.controller.js +105 -0
  251. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  252. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  253. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  254. package/dist/xp-catalog/xp-area.service.js +168 -0
  255. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  256. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  257. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  258. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  259. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  260. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  261. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  262. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  263. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  264. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  265. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  266. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  267. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  268. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  269. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  270. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  271. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  272. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  273. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  274. package/dist/xp-catalog/xp-skill.service.js +174 -0
  275. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  276. package/hedhog/data/evaluation_topic.yaml +17 -0
  277. package/hedhog/data/menu.yaml +91 -7
  278. package/hedhog/data/queue_definition.yaml +48 -0
  279. package/hedhog/data/route.yaml +511 -29
  280. package/hedhog/data/setting_group.yaml +20 -20
  281. package/hedhog/data/xp_area.yaml +164 -0
  282. package/hedhog/data/xp_learning_type.yaml +131 -0
  283. package/hedhog/data/xp_skill.yaml +1834 -0
  284. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  285. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  286. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1749 -0
  287. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  288. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  289. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  290. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  291. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  292. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +73 -8
  293. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  294. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  295. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  296. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1172 -0
  297. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  298. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  299. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  300. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  301. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +87 -46
  302. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  303. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  304. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  305. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +618 -480
  306. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +672 -737
  307. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  308. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  309. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  310. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  311. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +24 -10
  312. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  313. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  314. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  315. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  316. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  317. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  318. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  319. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +53 -0
  320. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  321. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +80 -1
  322. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  323. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  324. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  325. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  326. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  327. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  328. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  329. package/hedhog/frontend/app/courses/page.tsx.ejs +445 -230
  330. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  331. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  332. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  333. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  334. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  335. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  336. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  337. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  338. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  339. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  340. package/hedhog/frontend/messages/en.json +412 -31
  341. package/hedhog/frontend/messages/pt.json +412 -31
  342. package/hedhog/table/course_export.yaml +62 -0
  343. package/hedhog/table/lesson_xp_map.yaml +50 -0
  344. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  345. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  346. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  347. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  348. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  349. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  350. package/hedhog/table/student_area_xp.yaml +30 -0
  351. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  352. package/hedhog/table/student_skill_xp.yaml +30 -0
  353. package/hedhog/table/student_xp_event.yaml +34 -0
  354. package/hedhog/table/xp_area.yaml +39 -0
  355. package/hedhog/table/xp_learning_type.yaml +34 -0
  356. package/hedhog/table/xp_skill.yaml +39 -0
  357. package/package.json +13 -8
  358. package/src/bitcode-wallet/bitcode-wallet.service.ts +152 -0
  359. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  360. package/src/course/course-audio-transcription.service.ts +58 -21
  361. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  362. package/src/course/course-export-scorm12.service.ts +668 -0
  363. package/src/course/course-export.service.ts +280 -0
  364. package/src/course/course-lesson.controller.ts +6 -1
  365. package/src/course/course-structure.controller.ts +23 -1
  366. package/src/course/course-structure.service.ts +273 -7
  367. package/src/course/course-video-conversion.service.ts +113 -75
  368. package/src/course/course-video-hls.service.ts +946 -0
  369. package/src/course/course.controller.ts +54 -21
  370. package/src/course/course.mcp-tools.ts +1 -1
  371. package/src/course/course.module.ts +13 -0
  372. package/src/course/course.service.ts +906 -76
  373. package/src/course/dto/cleanup-course-storage.dto.ts +23 -0
  374. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  375. package/src/course/dto/create-course-bulk-job.dto.ts +10 -0
  376. package/src/course/dto/create-course-export.dto.ts +56 -0
  377. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  378. package/src/course/lms-bulk-upload-automation.service.ts +707 -0
  379. package/src/course/lms-bulk-upload-infra.service.ts +360 -0
  380. package/src/course/lms-bulk-upload.constants.ts +5 -0
  381. package/src/course/lms-bulk-upload.controller.ts +110 -4
  382. package/src/course/lms-bulk-upload.service.ts +1092 -204
  383. package/src/course/lms-setting.controller.ts +26 -8
  384. package/src/course/scorm12-schemas.ts +9 -0
  385. package/src/enterprise/training/training-student.service.ts +221 -2
  386. package/src/evaluation/evaluation.service.ts +123 -0
  387. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  388. package/src/exam/exam.module.ts +2 -1
  389. package/src/exam/exam.service.ts +86 -0
  390. package/src/exam/question.controller.ts +28 -0
  391. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  392. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  393. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  394. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  395. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  396. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +570 -0
  397. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  398. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  399. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  400. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  401. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  402. package/src/lms-commerce-access.subscriber.ts +88 -0
  403. package/src/lms.module.ts +21 -5
  404. package/src/platforma/dto/update-profile.dto.ts +59 -0
  405. package/src/platforma/platforma-video.service.ts +346 -0
  406. package/src/platforma/platforma.controller.ts +152 -3
  407. package/src/platforma/platforma.service.ts +268 -0
  408. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  409. package/src/student-xp/student-xp.controller.ts +92 -0
  410. package/src/student-xp/student-xp.module.ts +12 -0
  411. package/src/student-xp/student-xp.service.ts +318 -0
  412. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  413. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  414. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  415. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  416. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  417. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  418. package/src/xp-catalog/xp-area.controller.ts +64 -0
  419. package/src/xp-catalog/xp-area.service.ts +196 -0
  420. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  421. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  422. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  423. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  424. package/src/xp-catalog/xp-skill.service.ts +205 -0
  425. package/hedhog/data/video_resolution_profile.yaml +0 -7
  426. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  427. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  428. package/hedhog/table/video_resolution_profile.yaml +0 -18
  429. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  430. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  431. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  432. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  433. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  434. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -6,12 +6,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
6
6
  import {
7
7
  AlertTriangle,
8
8
  BookOpen,
9
+ Briefcase,
9
10
  CheckCircle2,
10
11
  CircleDot,
11
12
  Clock,
12
13
  Download,
13
14
  Layers,
14
15
  Loader2,
16
+ Mic,
15
17
  Pencil,
16
18
  Plus,
17
19
  Save,
@@ -19,6 +21,7 @@ import {
19
21
  UploadCloud,
20
22
  Video,
21
23
  X,
24
+ Zap,
22
25
  } from 'lucide-react';
23
26
  import { useTranslations } from 'next-intl';
24
27
  import { useRouter } from 'next/navigation';
@@ -28,12 +31,21 @@ import { toast } from 'sonner';
28
31
  import { z } from 'zod';
29
32
 
30
33
  import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
31
- import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
32
34
  import { FileTypeIcon } from '@/components/file-type-icon';
33
35
  import { RichTextEditor } from '@/components/rich-text-editor';
36
+ import {
37
+ AlertDialog,
38
+ AlertDialogAction,
39
+ AlertDialogCancel,
40
+ AlertDialogContent,
41
+ AlertDialogDescription,
42
+ AlertDialogFooter,
43
+ AlertDialogHeader,
44
+ AlertDialogTitle,
45
+ } from '@/components/ui/alert-dialog';
34
46
  import { Badge } from '@/components/ui/badge';
35
47
  import { Button } from '@/components/ui/button';
36
- import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
48
+ import { Checkbox } from '@/components/ui/checkbox';
37
49
  import {
38
50
  Dialog,
39
51
  DialogContent,
@@ -42,6 +54,15 @@ import {
42
54
  DialogHeader,
43
55
  DialogTitle,
44
56
  } from '@/components/ui/dialog';
57
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
58
+ import {
59
+ DropdownMenu,
60
+ DropdownMenuContent,
61
+ DropdownMenuItem,
62
+ DropdownMenuLabel,
63
+ DropdownMenuSeparator,
64
+ DropdownMenuTrigger,
65
+ } from '@/components/ui/dropdown-menu';
45
66
  import { EntityPicker } from '@/components/ui/entity-picker';
46
67
  import {
47
68
  Form,
@@ -74,6 +95,7 @@ import { cn } from '@/lib/utils';
74
95
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
75
96
  import { useMutation, useQueryClient } from '@tanstack/react-query';
76
97
 
98
+ import { CourseDeleteDialog } from '../../../_components/CourseDeleteDialog';
77
99
  import type {
78
100
  CourseEditFormValues,
79
101
  PickerOption,
@@ -83,7 +105,6 @@ import { CourseClassificationCard } from '../../_components/CourseClassification
83
105
  import { CourseContentCard } from '../../_components/CourseContentCard';
84
106
  import { CourseDangerZoneCard } from '../../_components/CourseDangerZoneCard';
85
107
  import { CourseFlagsCard } from '../../_components/CourseFlagsCard';
86
- import { CourseInstructorsSummaryCard } from './CourseInstructorsSummaryCard';
87
108
  import { CourseMediaCard } from '../../_components/CourseMediaCard';
88
109
  import { CourseRelationsCard } from '../../_components/CourseRelationsCard';
89
110
  import {
@@ -94,8 +115,14 @@ import {
94
115
  } from '../_data/services/course-structure.service';
95
116
  import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
96
117
  import { courseStructureQueryKey } from '../_data/use-course-structure-query';
97
- import { IconActionTooltip } from './icon-action-tooltip';
118
+ import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
119
+ import { CourseExportSheet } from './course-export-sheet';
120
+ import { CourseExportsTab } from './course-exports-tab';
98
121
  import { CourseOperationsTab } from './course-operations-tab';
122
+ import { CourseOverviewTab } from './course-overview-tab';
123
+ import { CourseXpOverviewTab } from './course-xp-overview-tab';
124
+ import { CourseInstructorsSummaryCard } from './CourseInstructorsSummaryCard';
125
+ import { IconActionTooltip } from './icon-action-tooltip';
99
126
  import { useStructureStore } from './store';
100
127
  import { Resource } from './types';
101
128
 
@@ -129,6 +156,8 @@ type ApiCourseDetail = {
129
156
  instructors?: Array<{ id: number; name: string; avatarId: number | null }>;
130
157
  logoFileId?: number | null;
131
158
  logoFilename?: string | null;
159
+ logoFileSize?: number | null;
160
+ logoMimeType?: string | null;
132
161
  logoImageType?: {
133
162
  suggestedWidth: number | null;
134
163
  suggestedHeight: number | null;
@@ -137,6 +166,8 @@ type ApiCourseDetail = {
137
166
  } | null;
138
167
  bannerFileId?: number | null;
139
168
  bannerFilename?: string | null;
169
+ bannerFileSize?: number | null;
170
+ bannerMimeType?: string | null;
140
171
  bannerImageType?: {
141
172
  suggestedWidth: number | null;
142
173
  suggestedHeight: number | null;
@@ -147,13 +178,26 @@ type ApiCourseDetail = {
147
178
  operationsProjectId?: number | null;
148
179
  operationsProjectCode?: string | null;
149
180
  operationsProjectName?: string | null;
181
+ deletionImpact?: ApiCourseDeletionImpact;
150
182
  };
151
183
 
152
- type VideoProfileOption = {
153
- id: number;
154
- name: string;
155
- ffmpeg_params: string;
156
- status: string;
184
+ type ApiCourseDeletionImpact = {
185
+ fileCount: number;
186
+ totalBytes: number;
187
+ formattedSize: string;
188
+ countsBySource?: Record<string, number>;
189
+ enrollmentCount?: number;
190
+ };
191
+
192
+ type ApiQueuedDeleteResponse = {
193
+ success: boolean;
194
+ status: 'queued';
195
+ queueJobId: number;
196
+ notificationId: number;
197
+ deletionImpact?: Pick<
198
+ ApiCourseDeletionImpact,
199
+ 'fileCount' | 'totalBytes' | 'formattedSize'
200
+ >;
157
201
  };
158
202
 
159
203
  type ApiCategory = { id: number; slug: string; name: string };
@@ -316,6 +360,24 @@ function formatFileSize(bytes: number): string {
316
360
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
317
361
  }
318
362
 
363
+ function formatBytes(bytes: number) {
364
+ if (!Number.isFinite(bytes) || bytes <= 0) {
365
+ return '0 B';
366
+ }
367
+
368
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
369
+ let value = bytes;
370
+ let unitIndex = 0;
371
+
372
+ while (value >= 1024 && unitIndex < units.length - 1) {
373
+ value /= 1024;
374
+ unitIndex += 1;
375
+ }
376
+
377
+ const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
378
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
379
+ }
380
+
319
381
  function mapApiCourseResourceToLocal(
320
382
  item: CourseResourceApi
321
383
  ): CourseResourceItem {
@@ -378,9 +440,86 @@ function buildSchema(t: (key: string) => string) {
378
440
  });
379
441
  }
380
442
 
443
+ // ── Skeleton ──────────────────────────────────────────────────────────────────
444
+
445
+ function EditorCourseSkeleton() {
446
+ return (
447
+ <div className="flex flex-col h-full min-h-0">
448
+ {/* Header */}
449
+ <div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
450
+ <Skeleton className="size-9 rounded-lg shrink-0" />
451
+ <div className="flex-1 min-w-0 flex flex-col gap-1">
452
+ <Skeleton className="h-4 w-16" />
453
+ <Skeleton className="h-3 w-32" />
454
+ </div>
455
+ <div className="flex items-center gap-1.5">
456
+ <Skeleton className="h-5 w-12 rounded-full" />
457
+ <Skeleton className="h-5 w-12 rounded-full" />
458
+ <Skeleton className="h-5 w-14 rounded-full" />
459
+ <Skeleton className="h-5 w-16 rounded-full" />
460
+ </div>
461
+ </div>
462
+ {/* Tabs bar */}
463
+ <div className="flex items-center gap-1 px-3 py-1 border-b bg-muted/50 shrink-0 overflow-hidden">
464
+ {Array.from({ length: 6 }).map((_, i) => (
465
+ <Skeleton key={i} className="h-7 w-16 rounded-md shrink-0" />
466
+ ))}
467
+ </div>
468
+ {/* Tab content — estrutura */}
469
+ <div className="flex-1 min-h-0 overflow-y-auto p-3 flex flex-col gap-3">
470
+ <div className="rounded-lg border bg-muted/20 p-3 flex flex-col gap-3">
471
+ <Skeleton className="h-3 w-28" />
472
+ {[80, 56, 96].map((w, i) => (
473
+ <div key={i} className="flex flex-col gap-1">
474
+ <Skeleton className="h-3 w-20" />
475
+ <Skeleton className={`h-8 w-full max-w-[${w}%]`} />
476
+ </div>
477
+ ))}
478
+ </div>
479
+ <div className="rounded-lg border bg-muted/20 p-3 flex flex-col gap-3">
480
+ <Skeleton className="h-3 w-24" />
481
+ <div className="flex flex-col gap-1">
482
+ <Skeleton className="h-3 w-16" />
483
+ <Skeleton className="h-20 w-full" />
484
+ </div>
485
+ </div>
486
+ <div className="rounded-lg border bg-muted/20 p-3 flex flex-col gap-3">
487
+ <Skeleton className="h-3 w-20" />
488
+ <div className="grid grid-cols-2 gap-2">
489
+ {[1, 2, 3, 4].map((i) => (
490
+ <div key={i} className="flex flex-col gap-1">
491
+ <Skeleton className="h-3 w-14" />
492
+ <Skeleton className="h-8 w-full" />
493
+ </div>
494
+ ))}
495
+ </div>
496
+ </div>
497
+ </div>
498
+ </div>
499
+ );
500
+ }
501
+
381
502
  // ── Component ─────────────────────────────────────────────────────────────────
382
503
 
383
- export function EditorCourse() {
504
+ const COURSE_TABS = [
505
+ 'visao-geral',
506
+ 'detalhes',
507
+ 'xp',
508
+ 'midia',
509
+ 'recursos',
510
+ 'operacoes',
511
+ 'publicacao',
512
+ 'extra',
513
+ 'exportacoes',
514
+ ] as const;
515
+ type CourseTab = (typeof COURSE_TABS)[number];
516
+
517
+ interface EditorCourseProps {
518
+ defaultTab?: string;
519
+ onTabChange?: (tab: string) => void;
520
+ }
521
+
522
+ export function EditorCourse({ defaultTab, onTabChange }: EditorCourseProps) {
384
523
  const courseId = useStructureStore((s) => s.courseId);
385
524
  const course = useStructureStore((s) => s.course);
386
525
  const sessions = useStructureStore((s) => s.sessions);
@@ -389,6 +528,7 @@ export function EditorCourse() {
389
528
 
390
529
  const { request, currentLocaleCode, locales } = useApp();
391
530
  const t = useTranslations('lms.CursoEditPage');
531
+ const courseListT = useTranslations('lms.CoursesPage');
392
532
  const courseLocaleLabel = currentLocaleCode?.startsWith('en')
393
533
  ? 'Course Language'
394
534
  : 'Idioma do Curso';
@@ -396,9 +536,24 @@ export function EditorCourse() {
396
536
  const isMobile = useIsMobile();
397
537
  const queryClient = useQueryClient();
398
538
  const createSessionMutation = useCreateSessionMutation();
539
+ const lmsSettings = useLmsSettingsQuery();
399
540
 
400
541
  // ── UI state ────────────────────────────────────────────────────────────────
401
- const [activeTab, setActiveTab] = useState('estrutura');
542
+ const [activeTab, setActiveTab] = useState<CourseTab>(() =>
543
+ defaultTab && (COURSE_TABS as readonly string[]).includes(defaultTab)
544
+ ? (defaultTab as CourseTab)
545
+ : 'visao-geral'
546
+ );
547
+ const [exportSheetOpen, setExportSheetOpen] = useState(false);
548
+ const [pendingBulkJob, setPendingBulkJob] = useState<'transcription' | 'xp_recalculation' | 'video_processing' | null>(null);
549
+ const [isBulkJobPending, setIsBulkJobPending] = useState(false);
550
+ const [videoProcessingStats, setVideoProcessingStats] = useState<{
551
+ withOriginal: number;
552
+ withoutOriginal: number;
553
+ alreadyProcessed: number;
554
+ } | null>(null);
555
+ const [isFetchingVideoStats, setIsFetchingVideoStats] = useState(false);
556
+ const [reprocessAlreadyProcessed, setReprocessAlreadyProcessed] = useState(false);
402
557
  const [categoryEditSheetOpen, setCategoryEditSheetOpen] = useState(false);
403
558
  const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
404
559
  null
@@ -417,7 +572,15 @@ export function EditorCourse() {
417
572
  >('active');
418
573
  const [savingCategoryEdit, setSavingCategoryEdit] = useState(false);
419
574
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
420
- const [deleting, setDeleting] = useState(false);
575
+ const [confirmationInput, setConfirmationInput] = useState('');
576
+ const [archivingCourse, setArchivingCourse] = useState(false);
577
+ const [queuingDelete, setQueuingDelete] = useState(false);
578
+ const [loadingDeleteImpact, setLoadingDeleteImpact] = useState(false);
579
+ const [deleteImpact, setDeleteImpact] =
580
+ useState<ApiCourseDeletionImpact | null>(null);
581
+ const [deleteCourseStatus, setDeleteCourseStatus] = useState<
582
+ CourseEditFormValues['status'] | null
583
+ >(null);
421
584
  const [logoPreview, setLogoPreview] = useState<string | null>(null);
422
585
  const [bannerPreview, setBannerPreview] = useState<string | null>(null);
423
586
  const [uploadingLogo, setUploadingLogo] = useState(false);
@@ -439,33 +602,23 @@ export function EditorCourse() {
439
602
  const [isUploadingResources, setIsUploadingResources] = useState(false);
440
603
  const [isSavingResources, setIsSavingResources] = useState(false);
441
604
  const resourcesInputRef = useRef<HTMLInputElement>(null);
442
- const [linkedProfileIds, setLinkedProfileIds] = useState<number[]>([]);
443
- const [videoProfilePickerResetKey, setVideoProfilePickerResetKey] =
444
- useState(0);
445
- const [videoProfileEditSheetOpen, setVideoProfileEditSheetOpen] =
446
- useState(false);
447
- const [editingVideoProfileId, setEditingVideoProfileId] = useState<
448
- number | null
449
- >(null);
450
- const [editingVideoProfileName, setEditingVideoProfileName] = useState('');
451
- const [editingVideoProfileParams, setEditingVideoProfileParams] =
452
- useState('');
453
- const [savingVideoProfileEdit, setSavingVideoProfileEdit] = useState(false);
454
605
 
455
606
  // ── Queries ─────────────────────────────────────────────────────────────────
456
- const { data: apiCourse, refetch: refetchCourse } = useQuery<ApiCourseDetail>(
457
- {
458
- queryKey: ['lms-course-detail', courseId],
459
- enabled: Boolean(courseId),
460
- queryFn: async () => {
461
- const response = await request<ApiCourseDetail>({
462
- url: `/lms/courses/${courseId}`,
463
- method: 'GET',
464
- });
465
- return response.data;
466
- },
467
- }
468
- );
607
+ const {
608
+ data: apiCourse,
609
+ refetch: refetchCourse,
610
+ isLoading: isLoadingCourse,
611
+ } = useQuery<ApiCourseDetail>({
612
+ queryKey: ['lms-course-detail', courseId],
613
+ enabled: Boolean(courseId),
614
+ queryFn: async () => {
615
+ const response = await request<ApiCourseDetail>({
616
+ url: `/lms/courses/${courseId}`,
617
+ method: 'GET',
618
+ });
619
+ return response.data;
620
+ },
621
+ });
469
622
 
470
623
  const { data: categoryListData, refetch: refetchCategoryOptions } =
471
624
  useQuery<ApiCategoryList>({
@@ -536,22 +689,6 @@ export function EditorCourse() {
536
689
  initialData: { data: [] },
537
690
  });
538
691
 
539
- const {
540
- data: allVideoProfiles = [],
541
- refetch: refetchVideoProfiles,
542
- isFetching: isFetchingVideoProfiles,
543
- } = useQuery<VideoProfileOption[]>({
544
- queryKey: ['lms-video-resolution-profiles-all'],
545
- queryFn: async () => {
546
- const response = await request<VideoProfileOption[]>({
547
- url: '/lms/video-resolution-profiles/all',
548
- method: 'GET',
549
- });
550
- return response.data;
551
- },
552
- initialData: [],
553
- });
554
-
555
692
  const { data: operationsConfig } = useQuery<{ isAvailable: boolean }>({
556
693
  queryKey: ['lms-course-operations-config', courseId],
557
694
  enabled: Boolean(courseId),
@@ -571,6 +708,12 @@ export function EditorCourse() {
571
708
 
572
709
  const showOperationsTab = operationsConfig?.isAvailable === true;
573
710
 
711
+ useEffect(() => {
712
+ if (apiCourse?.status) {
713
+ setDeleteCourseStatus(toPtStatus(apiCourse.status));
714
+ }
715
+ }, [apiCourse?.status]);
716
+
574
717
  // ── Save mutation ───────────────────────────────────────────────────────────
575
718
  const { mutate: saveCourse, isPending: saving } = useMutation({
576
719
  mutationFn: async (data: CourseEditFormValues) => {
@@ -607,15 +750,6 @@ export function EditorCourse() {
607
750
  return data;
608
751
  },
609
752
  onSuccess: (data) => {
610
- if (courseId) {
611
- void request({
612
- url: `/lms/courses/${courseId}/video-resolution-profiles/sync`,
613
- method: 'POST',
614
- data: { profileIds: linkedProfileIds },
615
- }).catch(() => {
616
- toast.error('Erro ao sincronizar perfis de vídeo.');
617
- });
618
- }
619
753
  setPersistedCertificateModel(data.modeloCertificado || '');
620
754
  updateCourseInStore({
621
755
  code: data.code,
@@ -759,14 +893,6 @@ export function EditorCourse() {
759
893
  );
760
894
  }, [certificateTemplateData, createdTemplateOptions]);
761
895
 
762
- const availableVideoProfiles = useMemo(
763
- () =>
764
- allVideoProfiles.filter(
765
- (profile) => !linkedProfileIds.includes(profile.id)
766
- ),
767
- [allVideoProfiles, linkedProfileIds]
768
- );
769
-
770
896
  // ── Structural stats ─────────────────────────────────────────────────────────
771
897
  const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
772
898
  const hours = Math.floor(totalMinutes / 60);
@@ -777,9 +903,6 @@ export function EditorCourse() {
777
903
  ).length;
778
904
 
779
905
  const formValues = form.watch();
780
- const showVideoTab =
781
- formValues.tipoOferta === 'sob_demanda' ||
782
- formValues.tipoOferta === 'hibrido';
783
906
  const descriptionText = String(formValues.descricaoPublica ?? '')
784
907
  .replace(/<[^>]+>/g, '')
785
908
  .trim();
@@ -911,32 +1034,6 @@ export function EditorCourse() {
911
1034
  };
912
1035
  }, [courseId, request]);
913
1036
 
914
- useEffect(() => {
915
- if (!showVideoTab && activeTab === 'videos') {
916
- setActiveTab('estrutura');
917
- }
918
- }, [showVideoTab, activeTab]);
919
-
920
- useEffect(() => {
921
- if (!courseId) return;
922
- let active = true;
923
- void request<VideoProfileOption[]>({
924
- url: `/lms/courses/${courseId}/video-resolution-profiles`,
925
- method: 'GET',
926
- })
927
- .then((res) => {
928
- if (!active) return;
929
- setLinkedProfileIds((res.data ?? []).map((p) => p.id));
930
- })
931
- .catch(() => {
932
- if (!active) return;
933
- setLinkedProfileIds([]);
934
- });
935
- return () => {
936
- active = false;
937
- };
938
- }, [courseId, request]);
939
-
940
1037
  async function syncCourseResources(nextResources: CourseResourceItem[]) {
941
1038
  setIsSavingResources(true);
942
1039
  try {
@@ -1256,38 +1353,6 @@ export function EditorCourse() {
1256
1353
  }
1257
1354
  }
1258
1355
 
1259
- function handleEditVideoProfile(profileId: number) {
1260
- const profile = allVideoProfiles.find((p) => p.id === profileId);
1261
- if (!profile) return;
1262
- setEditingVideoProfileId(profileId);
1263
- setEditingVideoProfileName(profile.name);
1264
- setEditingVideoProfileParams(profile.ffmpeg_params);
1265
- setVideoProfileEditSheetOpen(true);
1266
- }
1267
-
1268
- async function handleSaveVideoProfileEdit() {
1269
- if (!editingVideoProfileId) return;
1270
- try {
1271
- setSavingVideoProfileEdit(true);
1272
- await request({
1273
- url: `/lms/video-resolution-profiles/${editingVideoProfileId}`,
1274
- method: 'PATCH',
1275
- data: {
1276
- name: editingVideoProfileName,
1277
- ffmpeg_params: editingVideoProfileParams,
1278
- },
1279
- });
1280
- await refetchVideoProfiles();
1281
- setVideoProfileEditSheetOpen(false);
1282
- setEditingVideoProfileId(null);
1283
- toast.success('Perfil de vídeo atualizado com sucesso.');
1284
- } catch {
1285
- toast.error('Erro ao atualizar o perfil de vídeo.');
1286
- } finally {
1287
- setSavingVideoProfileEdit(false);
1288
- }
1289
- }
1290
-
1291
1356
  async function handleEditCategory(categorySlug: string) {
1292
1357
  const category = (categoryListData?.data ?? []).find(
1293
1358
  (item) => item.slug === categorySlug
@@ -1380,58 +1445,197 @@ export function EditorCourse() {
1380
1445
  }
1381
1446
  }
1382
1447
 
1383
- async function handleDelete() {
1384
- setDeleting(true);
1448
+ const deleteCourseTitle = apiCourse?.title?.trim() || course.title.trim();
1449
+ const deleteConfirmMatches = confirmationInput === deleteCourseTitle;
1450
+ const deleteRequiresArchive = deleteCourseStatus !== 'arquivado';
1451
+ const canDeleteCourse =
1452
+ Boolean(courseId) &&
1453
+ !deleteRequiresArchive &&
1454
+ deleteConfirmMatches &&
1455
+ !loadingDeleteImpact &&
1456
+ !queuingDelete &&
1457
+ !archivingCourse &&
1458
+ Boolean(deleteImpact);
1459
+
1460
+ async function loadDeleteImpact() {
1461
+ if (!courseId) return;
1462
+
1463
+ setLoadingDeleteImpact(true);
1464
+ setDeleteImpact(null);
1465
+
1385
1466
  try {
1386
- await request({ url: `/lms/courses/${courseId}`, method: 'DELETE' });
1387
- setDeleteDialogOpen(false);
1388
- toast.success(t('toasts.courseDeleted'));
1389
- router.push('/lms/courses');
1467
+ const response = await request<ApiCourseDetail>({
1468
+ url: `/lms/courses/${courseId}`,
1469
+ method: 'GET',
1470
+ });
1471
+
1472
+ const impact = response.data.deletionImpact;
1473
+
1474
+ setDeleteCourseStatus(toPtStatus(response.data.status));
1475
+ setDeleteImpact(
1476
+ impact
1477
+ ? {
1478
+ ...impact,
1479
+ formattedSize:
1480
+ impact.formattedSize || formatBytes(impact.totalBytes ?? 0),
1481
+ }
1482
+ : null
1483
+ );
1484
+ } catch {
1485
+ toast.error(courseListT('toasts.courseDeleteImpactLoadError'));
1390
1486
  } finally {
1391
- setDeleting(false);
1487
+ setLoadingDeleteImpact(false);
1392
1488
  }
1393
1489
  }
1394
1490
 
1395
- function onSubmit(data: CourseEditFormValues) {
1396
- if (data.status === 'publicado') {
1397
- const requiredProfileIds = new Set(linkedProfileIds);
1398
- const invalidLesson = lessons.find((lesson) => {
1399
- if (
1400
- lesson.type !== 'video' ||
1401
- lesson.videoProvider !== 'file_storage'
1402
- ) {
1403
- return false;
1404
- }
1491
+ function openDeleteDialog() {
1492
+ setConfirmationInput('');
1493
+ setDeleteImpact(null);
1494
+ setDeleteDialogOpen(true);
1495
+ void loadDeleteImpact();
1496
+ }
1405
1497
 
1406
- if (requiredProfileIds.size === 0) {
1407
- return true;
1408
- }
1498
+ async function archiveCourseForDelete() {
1499
+ if (!courseId || deleteCourseStatus === 'arquivado') return;
1409
1500
 
1410
- const resourceTypes = new Set(lesson.resources.map((res) => res.type));
1411
- return [...requiredProfileIds].some(
1412
- (profileId) => !resourceTypes.has(`video_profile:${profileId}`)
1413
- );
1501
+ setArchivingCourse(true);
1502
+ try {
1503
+ await request({
1504
+ url: `/lms/courses/${courseId}`,
1505
+ method: 'PATCH',
1506
+ data: { status: 'archived' },
1414
1507
  });
1415
1508
 
1416
- if (invalidLesson) {
1417
- toast.error(
1418
- linkedProfileIds.length === 0
1419
- ? 'Configure ao menos um perfil de vídeo antes de publicar o curso.'
1420
- : `A aula "${invalidLesson.title}" precisa de um vídeo para cada perfil antes da publicação.`
1421
- );
1422
- return;
1509
+ setDeleteCourseStatus('arquivado');
1510
+ form.setValue('status', 'arquivado', {
1511
+ shouldDirty: false,
1512
+ shouldTouch: false,
1513
+ shouldValidate: false,
1514
+ });
1515
+ toast.success(
1516
+ courseListT('toasts.courseArchived', { title: deleteCourseTitle })
1517
+ );
1518
+ await refetchCourse();
1519
+ await loadDeleteImpact();
1520
+ } catch {
1521
+ toast.error(courseListT('toasts.courseArchiveError'));
1522
+ } finally {
1523
+ setArchivingCourse(false);
1524
+ }
1525
+ }
1526
+
1527
+ async function handleDelete() {
1528
+ if (!courseId) return;
1529
+ if (confirmationInput !== deleteCourseTitle) {
1530
+ toast.error(
1531
+ courseListT('form.validationErrors.confirmationMismatch') ||
1532
+ 'Nome do curso não corresponde.'
1533
+ );
1534
+ return;
1535
+ }
1536
+
1537
+ if (deleteCourseStatus !== 'arquivado') {
1538
+ toast.error(courseListT('toasts.deleteOnlyArchived'));
1539
+ return;
1540
+ }
1541
+
1542
+ if (!deleteImpact) {
1543
+ toast.error(courseListT('toasts.courseDeleteImpactLoadError'));
1544
+ return;
1545
+ }
1546
+
1547
+ setQueuingDelete(true);
1548
+ try {
1549
+ const response = await request<ApiQueuedDeleteResponse>({
1550
+ url: `/lms/courses/${courseId}`,
1551
+ method: 'DELETE',
1552
+ });
1553
+
1554
+ setDeleteDialogOpen(false);
1555
+ toast.success(
1556
+ courseListT('toasts.courseDeleteQueued', {
1557
+ title: deleteCourseTitle,
1558
+ files:
1559
+ response.data?.deletionImpact?.fileCount ?? deleteImpact.fileCount,
1560
+ })
1561
+ );
1562
+ router.push('/lms/courses');
1563
+ } catch (error) {
1564
+ const message =
1565
+ error instanceof Error && error.message ? error.message : '';
1566
+
1567
+ if (message.includes('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED')) {
1568
+ toast.error(courseListT('toasts.deleteOnlyArchived'));
1569
+ } else {
1570
+ toast.error(courseListT('toasts.courseDeleteQueueError'));
1423
1571
  }
1572
+ } finally {
1573
+ setQueuingDelete(false);
1424
1574
  }
1575
+ }
1425
1576
 
1577
+ function onSubmit(data: CourseEditFormValues) {
1426
1578
  saveCourse(data);
1427
1579
  }
1428
1580
 
1581
+ function handlePublish() {
1582
+ const values = form.getValues();
1583
+ saveCourse({ ...values, status: 'publicado' });
1584
+ }
1585
+
1429
1586
  function onFormError(errors: Record<string, unknown>) {
1430
1587
  const first = Object.values(errors)[0] as { message?: string } | undefined;
1431
1588
  toast.error(first?.message ?? 'Verifique os campos obrigatórios');
1432
1589
  }
1433
1590
 
1591
+ // ── Bulk jobs ─────────────────────────────────────────────────────────────────
1592
+ const handleBulkJob = async (
1593
+ jobType: 'transcription' | 'xp_recalculation' | 'video_processing',
1594
+ opts?: { reprocessAlreadyProcessed?: boolean },
1595
+ ) => {
1596
+ setIsBulkJobPending(true);
1597
+ try {
1598
+ const res = await request<{ queued: number; skipped: number }>({
1599
+ url: `/lms/courses/${courseId}/structure/bulk-jobs`,
1600
+ method: 'POST',
1601
+ data: { jobType, ...opts },
1602
+ });
1603
+ toast.success(`${res.data?.queued ?? 0} job(s) enfileirado(s), ${res.data?.skipped ?? 0} ignorado(s).`);
1604
+ } catch {
1605
+ toast.error('Erro ao enfileirar jobs em massa.');
1606
+ } finally {
1607
+ setIsBulkJobPending(false);
1608
+ setPendingBulkJob(null);
1609
+ setVideoProcessingStats(null);
1610
+ }
1611
+ };
1612
+
1613
+ const handleOpenVideoProcessingDialog = async () => {
1614
+ setIsFetchingVideoStats(true);
1615
+ setReprocessAlreadyProcessed(false);
1616
+ try {
1617
+ const res = await request<{
1618
+ withOriginal: number;
1619
+ withoutOriginal: number;
1620
+ alreadyProcessed: number;
1621
+ }>({
1622
+ url: `/lms/courses/${courseId}/structure/video-processing-stats`,
1623
+ method: 'GET',
1624
+ });
1625
+ setVideoProcessingStats(res.data ?? null);
1626
+ setPendingBulkJob('video_processing');
1627
+ } catch {
1628
+ toast.error('Erro ao buscar estatísticas de processamento de vídeo.');
1629
+ } finally {
1630
+ setIsFetchingVideoStats(false);
1631
+ }
1632
+ };
1633
+
1434
1634
  // ── Render ────────────────────────────────────────────────────────────────────
1635
+ if (isLoadingCourse && !apiCourse) {
1636
+ return <EditorCourseSkeleton />;
1637
+ }
1638
+
1435
1639
  return (
1436
1640
  <Form {...form}>
1437
1641
  <form
@@ -1553,13 +1757,157 @@ export function EditorCourse() {
1553
1757
  </TooltipContent>
1554
1758
  </Tooltip>
1555
1759
  </TooltipProvider>
1760
+
1761
+ {/* ── Bulk jobs menu ────────────────────────────────────────── */}
1762
+ <DropdownMenu>
1763
+ <DropdownMenuTrigger asChild>
1764
+ <Button
1765
+ type="button"
1766
+ variant="ghost"
1767
+ size="icon"
1768
+ className="size-6 shrink-0"
1769
+ disabled={isBulkJobPending || isFetchingVideoStats}
1770
+ >
1771
+ {isBulkJobPending || isFetchingVideoStats ? (
1772
+ <Loader2 className="size-3.5 animate-spin" />
1773
+ ) : (
1774
+ <Briefcase className="size-3.5" />
1775
+ )}
1776
+ </Button>
1777
+ </DropdownMenuTrigger>
1778
+ <DropdownMenuContent align="end" className="w-56">
1779
+ <DropdownMenuLabel className="text-xs">Jobs em massa</DropdownMenuLabel>
1780
+ <DropdownMenuSeparator />
1781
+ {lmsSettings.transcriptionEnabled && (
1782
+ <DropdownMenuItem
1783
+ onSelect={() => setPendingBulkJob('transcription')}
1784
+ className="gap-2"
1785
+ >
1786
+ <Mic className="size-4 text-muted-foreground" />
1787
+ Refazer transcrição de todas as aulas
1788
+ </DropdownMenuItem>
1789
+ )}
1790
+ <DropdownMenuItem
1791
+ onSelect={() => setPendingBulkJob('xp_recalculation')}
1792
+ className="gap-2"
1793
+ >
1794
+ <Zap className="size-4 text-muted-foreground" />
1795
+ Recalcular XP de todas as aulas
1796
+ </DropdownMenuItem>
1797
+ <DropdownMenuItem
1798
+ onSelect={handleOpenVideoProcessingDialog}
1799
+ className="gap-2"
1800
+ >
1801
+ <Video className="size-4 text-muted-foreground" />
1802
+ Processar todos os vídeos
1803
+ </DropdownMenuItem>
1804
+ <DropdownMenuSeparator />
1805
+ <DropdownMenuLabel className="text-xs">Exportar</DropdownMenuLabel>
1806
+ <DropdownMenuItem
1807
+ onSelect={() => setExportSheetOpen(true)}
1808
+ className="gap-2"
1809
+ >
1810
+ <Download className="size-4 text-muted-foreground" />
1811
+ SCORM 1.2
1812
+ </DropdownMenuItem>
1813
+ </DropdownMenuContent>
1814
+ </DropdownMenu>
1815
+
1816
+ <AlertDialog
1817
+ open={pendingBulkJob === 'transcription' || pendingBulkJob === 'xp_recalculation'}
1818
+ onOpenChange={(open) => { if (!open) setPendingBulkJob(null); }}
1819
+ >
1820
+ <AlertDialogContent>
1821
+ <AlertDialogHeader>
1822
+ <AlertDialogTitle>
1823
+ {pendingBulkJob === 'transcription'
1824
+ ? 'Refazer transcrição de todas as aulas?'
1825
+ : 'Recalcular XP de todas as aulas?'}
1826
+ </AlertDialogTitle>
1827
+ <AlertDialogDescription>
1828
+ {pendingBulkJob === 'transcription'
1829
+ ? `Um job de transcrição será enfileirado para cada aula que possuir arquivo de áudio (${lessons.length} aulas no total). Aulas sem áudio serão ignoradas.`
1830
+ : `Um job de recálculo de XP será enfileirado para cada aula que possuir transcrição (${lessons.length} aulas no total). Aulas sem transcrição serão ignoradas.`}
1831
+ </AlertDialogDescription>
1832
+ </AlertDialogHeader>
1833
+ <AlertDialogFooter>
1834
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
1835
+ <AlertDialogAction
1836
+ onClick={() => pendingBulkJob && handleBulkJob(pendingBulkJob as 'transcription' | 'xp_recalculation')}
1837
+ >
1838
+ Confirmar
1839
+ </AlertDialogAction>
1840
+ </AlertDialogFooter>
1841
+ </AlertDialogContent>
1842
+ </AlertDialog>
1843
+
1844
+ <Dialog
1845
+ open={pendingBulkJob === 'video_processing'}
1846
+ onOpenChange={(open) => {
1847
+ if (!open) {
1848
+ setPendingBulkJob(null);
1849
+ setVideoProcessingStats(null);
1850
+ }
1851
+ }}
1852
+ >
1853
+ <DialogContent className="sm:max-w-md">
1854
+ <DialogHeader>
1855
+ <DialogTitle>Processar todos os vídeos?</DialogTitle>
1856
+ <DialogDescription>
1857
+ {videoProcessingStats ? (
1858
+ <>
1859
+ <span className="font-medium text-foreground">{videoProcessingStats.withOriginal}</span> aula(s) com vídeo original elegíveis para processamento.{' '}
1860
+ <span className="font-medium text-foreground">{videoProcessingStats.withoutOriginal}</span> aula(s) sem vídeo original serão ignoradas.
1861
+ </>
1862
+ ) : (
1863
+ 'Carregando estatísticas...'
1864
+ )}
1865
+ </DialogDescription>
1866
+ </DialogHeader>
1867
+ {videoProcessingStats && videoProcessingStats.alreadyProcessed > 0 && (
1868
+ <div className="flex items-center gap-2 py-1">
1869
+ <Checkbox
1870
+ id="reprocess-checkbox"
1871
+ checked={reprocessAlreadyProcessed}
1872
+ onCheckedChange={(checked) => setReprocessAlreadyProcessed(checked === true)}
1873
+ />
1874
+ <label htmlFor="reprocess-checkbox" className="text-sm leading-none cursor-pointer">
1875
+ Reprocessar aulas que já foram processadas{' '}
1876
+ <span className="text-muted-foreground">({videoProcessingStats.alreadyProcessed} aula(s))</span>
1877
+ </label>
1878
+ </div>
1879
+ )}
1880
+ <DialogFooter>
1881
+ <Button
1882
+ variant="outline"
1883
+ onClick={() => {
1884
+ setPendingBulkJob(null);
1885
+ setVideoProcessingStats(null);
1886
+ }}
1887
+ >
1888
+ Cancelar
1889
+ </Button>
1890
+ <Button
1891
+ disabled={isBulkJobPending || !videoProcessingStats}
1892
+ onClick={() => handleBulkJob('video_processing', { reprocessAlreadyProcessed })}
1893
+ >
1894
+ {isBulkJobPending && <Loader2 className="size-4 animate-spin mr-2" />}
1895
+ Confirmar
1896
+ </Button>
1897
+ </DialogFooter>
1898
+ </DialogContent>
1899
+ </Dialog>
1556
1900
  </div>
1557
1901
  </div>
1558
1902
 
1559
1903
  {/* ── Tabs ─────────────────────────────────────────────────────────── */}
1560
1904
  <Tabs
1561
1905
  value={activeTab}
1562
- onValueChange={setActiveTab}
1906
+ onValueChange={(v) => {
1907
+ const tab = v as CourseTab;
1908
+ setActiveTab(tab);
1909
+ onTabChange?.(tab);
1910
+ }}
1563
1911
  className="flex flex-1 min-h-0 min-w-0 flex-col"
1564
1912
  >
1565
1913
  <TabsList
@@ -1573,14 +1921,15 @@ export function EditorCourse() {
1573
1921
  >
1574
1922
  {(
1575
1923
  [
1576
- 'estrutura',
1577
- 'sobre',
1924
+ 'visao-geral',
1925
+ 'detalhes',
1926
+ 'xp',
1578
1927
  'midia',
1579
1928
  'recursos',
1580
- 'extra',
1581
- ...(showVideoTab ? ['videos'] : []),
1582
1929
  ...(showOperationsTab ? ['operacoes'] : []),
1583
1930
  'publicacao',
1931
+ 'extra',
1932
+ 'exportacoes',
1584
1933
  ] as const
1585
1934
  ).map((tab) => (
1586
1935
  <TabsTrigger
@@ -1593,30 +1942,40 @@ export function EditorCourse() {
1593
1942
  : 'h-8 px-3 text-xs'
1594
1943
  )}
1595
1944
  >
1596
- {tab === 'estrutura'
1597
- ? t('structureEditor.tabs.structure')
1598
- : tab === 'sobre'
1599
- ? t('structureEditor.tabs.about')
1600
- : tab === 'midia'
1601
- ? t('structureEditor.tabs.media')
1602
- : tab === 'recursos'
1603
- ? t('structureEditor.tabs.resources')
1604
- : tab === 'extra'
1605
- ? t('structureEditor.tabs.extra')
1606
- : tab === 'videos'
1607
- ? t('structureEditor.tabs.videoProfiles')
1945
+ {tab === 'visao-geral'
1946
+ ? t('structureEditor.tabs.overview')
1947
+ : tab === 'detalhes'
1948
+ ? t('structureEditor.tabs.details')
1949
+ : tab === 'xp'
1950
+ ? t('structureEditor.tabs.xp')
1951
+ : tab === 'midia'
1952
+ ? t('structureEditor.tabs.media')
1953
+ : tab === 'recursos'
1954
+ ? t('structureEditor.tabs.resources')
1955
+ : tab === 'extra'
1956
+ ? t('structureEditor.tabs.extra')
1608
1957
  : tab === 'operacoes'
1609
- ? 'Operações'
1610
- : t('structureEditor.tabs.publish')}
1958
+ ? 'Operações'
1959
+ : tab === 'exportacoes'
1960
+ ? 'Exportações'
1961
+ : t('structureEditor.tabs.publish')}
1611
1962
  </TabsTrigger>
1612
1963
  ))}
1613
1964
  </TabsList>
1614
1965
 
1615
1966
  <div className="flex-1 min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain">
1616
1967
  <div className="flex min-w-0 flex-col gap-3 p-2 sm:p-3">
1617
- {/* ── Tab: Estrutura ──────────────────────────────────────── */}
1968
+ {/* ── Tab: Visão Geral ─────────────────────────────────────── */}
1969
+ <TabsContent value="visao-geral" className="mt-0 min-w-0">
1970
+ <CourseOverviewTab
1971
+ courseId={courseId}
1972
+ locale={currentLocaleCode || 'pt-BR'}
1973
+ />
1974
+ </TabsContent>
1975
+
1976
+ {/* ── Tab: Detalhes ───────────────────────────────────────── */}
1618
1977
  <TabsContent
1619
- value="estrutura"
1978
+ value="detalhes"
1620
1979
  className="mt-0 flex min-w-0 flex-col gap-3"
1621
1980
  >
1622
1981
  {/* Dados principais */}
@@ -1828,13 +2187,6 @@ export function EditorCourse() {
1828
2187
  />
1829
2188
  </CardContent>
1830
2189
  </Card>
1831
- </TabsContent>
1832
-
1833
- {/* ── Tab: Sobre ──────────────────────────────────────────── */}
1834
- <TabsContent
1835
- value="sobre"
1836
- className="mt-0 flex min-w-0 flex-col gap-3"
1837
- >
1838
2190
  <CourseClassificationCard
1839
2191
  form={form}
1840
2192
  t={t}
@@ -1859,6 +2211,13 @@ export function EditorCourse() {
1859
2211
  <CourseContentCard form={form} compact />
1860
2212
  </TabsContent>
1861
2213
 
2214
+ <TabsContent value="xp" className="mt-0 min-w-0">
2215
+ <CourseXpOverviewTab
2216
+ courseId={courseId}
2217
+ locale={currentLocaleCode || 'pt-BR'}
2218
+ />
2219
+ </TabsContent>
2220
+
1862
2221
  {/* ── Tab: Mídia ──────────────────────────────────────────── */}
1863
2222
  <TabsContent value="midia" className="mt-0 min-w-0">
1864
2223
  <CourseMediaCard
@@ -1879,6 +2238,8 @@ export function EditorCourse() {
1879
2238
  name:
1880
2239
  apiCourse.logoFilename ||
1881
2240
  `#${apiCourse.logoFileId}`,
2241
+ size: apiCourse.logoFileSize ?? null,
2242
+ mimeType: apiCourse.logoMimeType ?? null,
1882
2243
  }
1883
2244
  : undefined
1884
2245
  }
@@ -1889,6 +2250,8 @@ export function EditorCourse() {
1889
2250
  name:
1890
2251
  apiCourse.bannerFilename ||
1891
2252
  `#${apiCourse.bannerFileId}`,
2253
+ size: apiCourse.bannerFileSize ?? null,
2254
+ mimeType: apiCourse.bannerMimeType ?? null,
1892
2255
  }
1893
2256
  : undefined
1894
2257
  }
@@ -2121,179 +2484,11 @@ export function EditorCourse() {
2121
2484
  <CourseFlagsCard form={form} t={t} compact />
2122
2485
  <CourseDangerZoneCard
2123
2486
  t={t}
2124
- onDelete={() => setDeleteDialogOpen(true)}
2487
+ onDelete={openDeleteDialog}
2125
2488
  compact
2126
2489
  />
2127
2490
  </TabsContent>
2128
2491
 
2129
- {/* ── Tab: Vídeos (on_demand / blended only) ─────────────── */}
2130
- {showVideoTab && (
2131
- <TabsContent
2132
- value="videos"
2133
- className="mt-0 flex min-w-0 flex-col gap-3"
2134
- >
2135
- <Card className="bg-muted/20 py-2 gap-2">
2136
- <CardHeader className="px-3 pt-2 pb-0">
2137
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
2138
- {t('structureEditor.videoProfiles.title')}
2139
- </CardTitle>
2140
- </CardHeader>
2141
- <CardContent className="px-3 pb-2 flex flex-col gap-2">
2142
- <EntityPicker
2143
- key={videoProfilePickerResetKey}
2144
- placeholder={t(
2145
- 'structureEditor.videoProfiles.placeholder'
2146
- )}
2147
- options={availableVideoProfiles}
2148
- getOptionValue={(opt) => opt.id}
2149
- getOptionLabel={(opt) => opt.name}
2150
- onChange={(_val, opt) => {
2151
- if (opt && !linkedProfileIds.includes(opt.id)) {
2152
- setLinkedProfileIds((prev) => [...prev, opt.id]);
2153
- setVideoProfilePickerResetKey(
2154
- (current) => current + 1
2155
- );
2156
- }
2157
- }}
2158
- showCreateButton
2159
- createTitle={t(
2160
- 'structureEditor.videoProfiles.createTitle'
2161
- )}
2162
- createFields={[
2163
- {
2164
- name: 'name',
2165
- label: t(
2166
- 'structureEditor.videoProfiles.createFields.name.label'
2167
- ),
2168
- placeholder: t(
2169
- 'structureEditor.videoProfiles.createFields.name.placeholder'
2170
- ),
2171
- required: true,
2172
- },
2173
- {
2174
- name: 'ffmpeg_params',
2175
- label: t(
2176
- 'structureEditor.videoProfiles.createFields.ffmpegParams.label'
2177
- ),
2178
- placeholder: t(
2179
- 'structureEditor.videoProfiles.createFields.ffmpegParams.placeholder'
2180
- ),
2181
- required: true,
2182
- },
2183
- ]}
2184
- onCreate={async (values) => {
2185
- try {
2186
- const response = await request<VideoProfileOption>({
2187
- url: '/lms/video-resolution-profiles',
2188
- method: 'POST',
2189
- data: {
2190
- name: values.name,
2191
- ffmpeg_params: values.ffmpeg_params,
2192
- },
2193
- });
2194
- await refetchVideoProfiles();
2195
- return response.data;
2196
- } catch {
2197
- toast.error(
2198
- t('structureEditor.videoProfiles.createError')
2199
- );
2200
- return null;
2201
- }
2202
- }}
2203
- />
2204
-
2205
- {isFetchingVideoProfiles ? (
2206
- <div className="flex flex-col gap-1">
2207
- {Array.from({ length: 3 }).map((_, index) => (
2208
- <div
2209
- key={`video-profile-skeleton-${index}`}
2210
- className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
2211
- >
2212
- <Skeleton className="size-3.5 rounded-full shrink-0" />
2213
- <Skeleton className="h-3 w-full max-w-56" />
2214
- </div>
2215
- ))}
2216
- </div>
2217
- ) : linkedProfileIds.length === 0 ? (
2218
- <p className="text-center text-xs text-muted-foreground py-1">
2219
- {t('structureEditor.videoProfiles.empty')}
2220
- </p>
2221
- ) : (
2222
- <div className="flex flex-col gap-1">
2223
- {linkedProfileIds.map((profileId) => {
2224
- const profile = allVideoProfiles.find(
2225
- (p) => p.id === profileId
2226
- );
2227
- return (
2228
- <div
2229
- key={profileId}
2230
- className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
2231
- >
2232
- <Video className="size-3.5 shrink-0 text-violet-500" />
2233
- <div className="flex-1 min-w-0">
2234
- <p className="text-xs font-medium truncate">
2235
- {profile?.name?.trim()
2236
- ? profile.name
2237
- : t(
2238
- 'structureEditor.videoProfiles.fallbackName',
2239
- {
2240
- id: profileId,
2241
- }
2242
- )}
2243
- </p>
2244
- </div>
2245
- <IconActionTooltip
2246
- label={t(
2247
- 'structureEditor.videoProfiles.editAria'
2248
- )}
2249
- >
2250
- <Button
2251
- type="button"
2252
- variant="ghost"
2253
- size="icon"
2254
- className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
2255
- onClick={() =>
2256
- handleEditVideoProfile(profileId)
2257
- }
2258
- aria-label={t(
2259
- 'structureEditor.videoProfiles.editAria'
2260
- )}
2261
- >
2262
- <Pencil className="size-3" />
2263
- </Button>
2264
- </IconActionTooltip>
2265
- <IconActionTooltip
2266
- label={t(
2267
- 'structureEditor.videoProfiles.removeAria'
2268
- )}
2269
- >
2270
- <Button
2271
- type="button"
2272
- variant="ghost"
2273
- size="icon"
2274
- className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
2275
- onClick={() =>
2276
- setLinkedProfileIds((prev) =>
2277
- prev.filter((id) => id !== profileId)
2278
- )
2279
- }
2280
- aria-label={t(
2281
- 'structureEditor.videoProfiles.removeAria'
2282
- )}
2283
- >
2284
- <X className="size-3" />
2285
- </Button>
2286
- </IconActionTooltip>
2287
- </div>
2288
- );
2289
- })}
2290
- </div>
2291
- )}
2292
- </CardContent>
2293
- </Card>
2294
- </TabsContent>
2295
- )}
2296
-
2297
2492
  {/* ── Tab: Operações ──────────────────────────────────────── */}
2298
2493
  {showOperationsTab && (
2299
2494
  <TabsContent value="operacoes" className="mt-0 min-w-0">
@@ -2356,21 +2551,39 @@ export function EditorCourse() {
2356
2551
  {t('structureEditor.publishChecklist.statusTitle')}
2357
2552
  </CardTitle>
2358
2553
  </CardHeader>
2359
- <CardContent className="px-3 pb-2 flex items-center justify-between gap-2">
2360
- <p className="text-xs text-muted-foreground">
2361
- {t('structureEditor.publishChecklist.statusProgress', {
2362
- completed: completedRequired,
2363
- total: requiredChecklist.length,
2364
- })}
2365
- </p>
2366
- <Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
2367
- {isReadyToPublish
2368
- ? t('structureEditor.publishChecklist.ready')
2369
- : t('structureEditor.publishChecklist.notReady')}
2370
- </Badge>
2554
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
2555
+ <div className="flex items-center justify-between gap-2">
2556
+ <p className="text-xs text-muted-foreground">
2557
+ {t('structureEditor.publishChecklist.statusProgress', {
2558
+ completed: completedRequired,
2559
+ total: requiredChecklist.length,
2560
+ })}
2561
+ </p>
2562
+ <Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
2563
+ {isReadyToPublish
2564
+ ? t('structureEditor.publishChecklist.ready')
2565
+ : t('structureEditor.publishChecklist.notReady')}
2566
+ </Badge>
2567
+ </div>
2568
+ <Button
2569
+ type="button"
2570
+ size="sm"
2571
+ className="w-full h-8 text-xs"
2572
+ disabled={!isReadyToPublish || formValues.status === 'publicado' || saving}
2573
+ onClick={handlePublish}
2574
+ >
2575
+ {formValues.status === 'publicado'
2576
+ ? t('structureEditor.publishChecklist.alreadyPublished')
2577
+ : t('structureEditor.publishChecklist.publishButton')}
2578
+ </Button>
2371
2579
  </CardContent>
2372
2580
  </Card>
2373
2581
  </TabsContent>
2582
+
2583
+ {/* ── Tab: Exportações ────────────────────────────────────────── */}
2584
+ <TabsContent value="exportacoes" className="mt-0 min-w-0">
2585
+ <CourseExportsTab courseId={courseId} />
2586
+ </TabsContent>
2374
2587
  </div>
2375
2588
  </div>
2376
2589
  </Tabs>
@@ -2419,68 +2632,6 @@ export function EditorCourse() {
2419
2632
  </div>
2420
2633
  </form>
2421
2634
 
2422
- <Sheet
2423
- open={videoProfileEditSheetOpen}
2424
- onOpenChange={(open) => {
2425
- setVideoProfileEditSheetOpen(open);
2426
- if (!open) setEditingVideoProfileId(null);
2427
- }}
2428
- >
2429
- <ResizableSheetContent
2430
- sheetId="lms-course-structure-video-profile-edit-sheet"
2431
- defaultWidth={560}
2432
- minWidth={420}
2433
- maxWidth={920}
2434
- className="sm:max-w-lg"
2435
- >
2436
- <SheetHeader>
2437
- <SheetTitle>
2438
- {t('structureEditor.videoProfiles.sheet.title')}
2439
- </SheetTitle>
2440
- <SheetDescription>
2441
- {t('structureEditor.videoProfiles.sheet.description')}
2442
- </SheetDescription>
2443
- </SheetHeader>
2444
- <div className="space-y-3 px-4 pb-4 pt-2">
2445
- <div className="space-y-1.5">
2446
- <FormLabel>
2447
- {t('structureEditor.videoProfiles.createFields.name.label')}
2448
- </FormLabel>
2449
- <Input
2450
- value={editingVideoProfileName}
2451
- onChange={(e) => setEditingVideoProfileName(e.target.value)}
2452
- placeholder={t(
2453
- 'structureEditor.videoProfiles.createFields.name.placeholder'
2454
- )}
2455
- />
2456
- </div>
2457
- <FfmpegParamsEditor
2458
- value={editingVideoProfileParams}
2459
- onChange={setEditingVideoProfileParams}
2460
- />
2461
- <div className="flex justify-end gap-2 pt-2">
2462
- <Button
2463
- type="button"
2464
- variant="outline"
2465
- onClick={() => setVideoProfileEditSheetOpen(false)}
2466
- disabled={savingVideoProfileEdit}
2467
- >
2468
- {t('structureEditor.videoProfiles.sheet.actions.cancel')}
2469
- </Button>
2470
- <Button
2471
- type="button"
2472
- disabled={savingVideoProfileEdit}
2473
- onClick={() => void handleSaveVideoProfileEdit()}
2474
- >
2475
- {savingVideoProfileEdit
2476
- ? t('structureEditor.videoProfiles.sheet.actions.saving')
2477
- : t('structureEditor.videoProfiles.sheet.actions.save')}
2478
- </Button>
2479
- </div>
2480
- </div>
2481
- </ResizableSheetContent>
2482
- </Sheet>
2483
-
2484
2635
  <Sheet
2485
2636
  open={categoryEditSheetOpen}
2486
2637
  onOpenChange={(open) => {
@@ -2556,53 +2707,40 @@ export function EditorCourse() {
2556
2707
  </ResizableSheetContent>
2557
2708
  </Sheet>
2558
2709
 
2559
- {/* ── Delete dialog ────────────────────────────────────────────────── */}
2560
- <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
2561
- <DialogContent>
2562
- <DialogHeader>
2563
- <DialogTitle className="flex items-center gap-2">
2564
- <AlertTriangle className="size-5 text-destructive" />
2565
- {t('deleteDialog.title')}
2566
- </DialogTitle>
2567
- <DialogDescription asChild>
2568
- <div className="flex flex-col gap-3">
2569
- <p>
2570
- {t('deleteDialog.description')}{' '}
2571
- <strong className="text-foreground">{course.title}</strong>?
2572
- </p>
2573
- {(apiCourse?.enrollmentCount ?? 0) > 0 ? (
2574
- <div className="flex items-center gap-2 rounded-md bg-amber-50 px-3 py-2.5 text-xs font-medium text-amber-700">
2575
- <AlertTriangle className="size-3.5 shrink-0" />
2576
- <span>
2577
- {t('deleteDialog.warning', {
2578
- students: apiCourse?.enrollmentCount ?? 0,
2579
- certificates: apiCourse?.certificatesIssued ?? 0,
2580
- })}
2581
- </span>
2582
- </div>
2583
- ) : null}
2584
- </div>
2585
- </DialogDescription>
2586
- </DialogHeader>
2587
- <DialogFooter className="gap-2">
2588
- <Button
2589
- variant="outline"
2590
- onClick={() => setDeleteDialogOpen(false)}
2591
- >
2592
- {t('deleteDialog.actions.cancel')}
2593
- </Button>
2594
- <Button
2595
- variant="destructive"
2596
- onClick={handleDelete}
2597
- disabled={deleting}
2598
- className="gap-2"
2599
- >
2600
- {deleting ? <Loader2 className="size-4 animate-spin" /> : null}
2601
- {t('deleteDialog.actions.delete')}
2602
- </Button>
2603
- </DialogFooter>
2604
- </DialogContent>
2605
- </Dialog>
2710
+ <CourseExportSheet
2711
+ open={exportSheetOpen}
2712
+ onOpenChange={setExportSheetOpen}
2713
+ courseId={courseId}
2714
+ sessionCount={sessions.length}
2715
+ videoLessonCount={lessons.filter((l) => l.type === 'video').length}
2716
+ />
2717
+
2718
+ <CourseDeleteDialog
2719
+ open={deleteDialogOpen}
2720
+ onOpenChange={(open) => {
2721
+ setDeleteDialogOpen(open);
2722
+ if (!open) {
2723
+ setConfirmationInput('');
2724
+ setDeleteImpact(null);
2725
+ }
2726
+ }}
2727
+ course={{
2728
+ title: deleteCourseTitle || course.title,
2729
+ status: deleteCourseStatus ?? 'rascunho',
2730
+ enrollmentCount:
2731
+ deleteImpact?.enrollmentCount ?? apiCourse?.enrollmentCount ?? 0,
2732
+ }}
2733
+ impact={deleteImpact}
2734
+ loadingImpact={loadingDeleteImpact}
2735
+ confirmationInput={confirmationInput}
2736
+ onConfirmationInputChange={setConfirmationInput}
2737
+ archiving={archivingCourse}
2738
+ queuingDelete={queuingDelete}
2739
+ canDelete={canDeleteCourse}
2740
+ onArchive={archiveCourseForDelete}
2741
+ onDelete={handleDelete}
2742
+ t={courseListT}
2743
+ />
2606
2744
  </Form>
2607
2745
  );
2608
2746
  }