@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
@@ -0,0 +1,1172 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from '@/components/ui/alert-dialog';
13
+ import { Badge } from '@/components/ui/badge';
14
+ import { Button } from '@/components/ui/button';
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardHeader,
20
+ CardTitle,
21
+ } from '@/components/ui/card';
22
+ import {
23
+ ChartContainer,
24
+ ChartTooltip,
25
+ ChartTooltipContent,
26
+ } from '@/components/ui/chart';
27
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
28
+ import { Skeleton } from '@/components/ui/skeleton';
29
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
30
+ import { useQueryClient } from '@tanstack/react-query';
31
+ import {
32
+ BookOpen,
33
+ Clapperboard,
34
+ Download,
35
+ FileArchive,
36
+ FileText,
37
+ Film,
38
+ HardDrive,
39
+ Image,
40
+ Layers,
41
+ Loader2,
42
+ Mic,
43
+ Paperclip,
44
+ Sparkles,
45
+ Target,
46
+ Trash2,
47
+ Zap,
48
+ type LucideIcon,
49
+ } from 'lucide-react';
50
+ import { useTranslations } from 'next-intl';
51
+ import { useEffect, useMemo, useState } from 'react';
52
+ import { Cell, Pie, PieChart } from 'recharts';
53
+ import { toast } from 'sonner';
54
+ import { useCourseContentOverviewQuery } from '../_data/use-course-content-overview';
55
+ import { useCourseXpOverviewQuery } from '../_data/use-course-xp-overview';
56
+
57
+ const TYPE_COLORS: Record<string, string> = {
58
+ video: '#0ea5e9',
59
+ questao: '#f97316',
60
+ post: '#22c55e',
61
+ };
62
+
63
+ const TYPE_GRADIENTS: Record<
64
+ string,
65
+ { from: string; icon: LucideIcon; label: string }
66
+ > = {
67
+ video: {
68
+ from: 'from-sky-500/12 via-blue-500/6',
69
+ icon: Clapperboard,
70
+ label: 'Vídeo',
71
+ },
72
+ questao: {
73
+ from: 'from-orange-500/12 via-amber-500/6',
74
+ icon: FileText,
75
+ label: 'Questão',
76
+ },
77
+ post: {
78
+ from: 'from-green-500/12 via-emerald-500/6',
79
+ icon: BookOpen,
80
+ label: 'Post',
81
+ },
82
+ };
83
+
84
+ const AREA_COLORS = ['#0f766e', '#0ea5e9', '#14b8a6', '#38bdf8', '#0891b2'];
85
+ const SKILL_COLORS = ['#ea580c', '#f97316', '#fb923c', '#fdba74', '#fed7aa'];
86
+
87
+ const STORAGE_CATEGORY_META: Record<
88
+ string,
89
+ {
90
+ label: string;
91
+ accentClassName: string;
92
+ icon: LucideIcon;
93
+ iconClassName: string;
94
+ iconBackgroundClassName: string;
95
+ }
96
+ > = {
97
+ video_original: {
98
+ label: 'Vídeos originais',
99
+ accentClassName: 'from-sky-500/20 via-blue-500/10 to-transparent',
100
+ icon: Film,
101
+ iconClassName: 'text-sky-600 dark:text-sky-400',
102
+ iconBackgroundClassName: 'bg-sky-500/10',
103
+ },
104
+ video_profile: {
105
+ label: 'Vídeos convertidos',
106
+ accentClassName: 'from-indigo-500/20 via-blue-500/10 to-transparent',
107
+ icon: Clapperboard,
108
+ iconClassName: 'text-indigo-600 dark:text-indigo-400',
109
+ iconBackgroundClassName: 'bg-indigo-500/10',
110
+ },
111
+ lesson_audio: {
112
+ label: 'Áudios',
113
+ accentClassName: 'from-teal-500/20 via-cyan-500/10 to-transparent',
114
+ icon: Mic,
115
+ iconClassName: 'text-teal-600 dark:text-teal-400',
116
+ iconBackgroundClassName: 'bg-teal-500/10',
117
+ },
118
+ extracted_image: {
119
+ label: 'Imagens extraídas',
120
+ accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
121
+ icon: Image,
122
+ iconClassName: 'text-amber-600 dark:text-amber-400',
123
+ iconBackgroundClassName: 'bg-amber-500/10',
124
+ },
125
+ student_download: {
126
+ label: 'Downloads do aluno',
127
+ accentClassName: 'from-violet-500/20 via-fuchsia-500/10 to-transparent',
128
+ icon: Download,
129
+ iconClassName: 'text-violet-600 dark:text-violet-400',
130
+ iconBackgroundClassName: 'bg-violet-500/10',
131
+ },
132
+ supplementary_material: {
133
+ label: 'Materiais de apoio',
134
+ accentClassName: 'from-fuchsia-500/20 via-pink-500/10 to-transparent',
135
+ icon: Paperclip,
136
+ iconClassName: 'text-fuchsia-600 dark:text-fuchsia-400',
137
+ iconBackgroundClassName: 'bg-fuchsia-500/10',
138
+ },
139
+ course_image: {
140
+ label: 'Imagens do curso',
141
+ accentClassName: 'from-rose-500/20 via-pink-500/10 to-transparent',
142
+ icon: Image,
143
+ iconClassName: 'text-rose-600 dark:text-rose-400',
144
+ iconBackgroundClassName: 'bg-rose-500/10',
145
+ },
146
+ course_file: {
147
+ label: 'Arquivos do curso',
148
+ accentClassName: 'from-slate-500/20 via-zinc-500/10 to-transparent',
149
+ icon: FileText,
150
+ iconClassName: 'text-slate-600 dark:text-slate-400',
151
+ iconBackgroundClassName: 'bg-slate-500/10',
152
+ },
153
+ other_lesson_file: {
154
+ label: 'Outros arquivos',
155
+ accentClassName: 'from-muted/80 via-muted/40 to-transparent',
156
+ icon: HardDrive,
157
+ iconClassName: 'text-muted-foreground',
158
+ iconBackgroundClassName: 'bg-muted',
159
+ },
160
+ course_export: {
161
+ label: 'Arquivos de exportação',
162
+ accentClassName: 'from-emerald-500/20 via-green-500/10 to-transparent',
163
+ icon: FileArchive,
164
+ iconClassName: 'text-emerald-600 dark:text-emerald-400',
165
+ iconBackgroundClassName: 'bg-emerald-500/10',
166
+ },
167
+ };
168
+
169
+ interface Props {
170
+ courseId: string;
171
+ locale: string;
172
+ }
173
+
174
+ type CleanupDialogState = {
175
+ categoryKey: string;
176
+ categoryLabel: string;
177
+ fileCount: number;
178
+ totalBytes: number;
179
+ };
180
+
181
+ type CleanupTrackedJobState = {
182
+ categoryKey: string;
183
+ queueJobId: number;
184
+ status: string;
185
+ };
186
+
187
+ export function CourseOverviewTab({ courseId, locale }: Props) {
188
+ const { request } = useApp();
189
+ const queryClient = useQueryClient();
190
+ const t = useTranslations('lms.CursoEditPage.structureEditor.courseOverview');
191
+ const { data, isLoading, isError } = useCourseContentOverviewQuery(courseId);
192
+ const { data: xpData } = useCourseXpOverviewQuery(courseId);
193
+ const [cleanupCategoryKey, setCleanupCategoryKey] = useState<string | null>(
194
+ null
195
+ );
196
+ const [cleanupDialogState, setCleanupDialogState] =
197
+ useState<CleanupDialogState | null>(null);
198
+ const [cleanupTrackedJob, setCleanupTrackedJob] =
199
+ useState<CleanupTrackedJobState | null>(null);
200
+
201
+ const { data: cleanupTrackedJobDetail, isError: isCleanupTrackedJobError } =
202
+ useQuery<{
203
+ id: number;
204
+ status: string;
205
+ last_error?: string | null;
206
+ } | null>({
207
+ queryKey: [
208
+ 'lms-course-storage-cleanup-job',
209
+ cleanupTrackedJob?.queueJobId,
210
+ ],
211
+ enabled: Boolean(cleanupTrackedJob?.queueJobId),
212
+ queryFn: async () => {
213
+ if (!cleanupTrackedJob?.queueJobId) return null;
214
+ const response = await request<{
215
+ id: number;
216
+ status: string;
217
+ last_error?: string | null;
218
+ }>({
219
+ url: `/queue/jobs/${cleanupTrackedJob.queueJobId}`,
220
+ method: 'GET',
221
+ });
222
+ return response.data;
223
+ },
224
+ refetchInterval: ({ state }) => {
225
+ const status = (state.data as { status?: string } | null)?.status;
226
+ if (!status) return 3000;
227
+ return ['pending', 'scheduled', 'processing', 'retrying'].includes(
228
+ status
229
+ )
230
+ ? 3000
231
+ : false;
232
+ },
233
+ });
234
+
235
+ const formatBytes = (bytes: number) => {
236
+ if (!Number.isFinite(bytes) || bytes <= 0) {
237
+ return '0 B';
238
+ }
239
+
240
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
241
+ let value = bytes;
242
+ let unitIndex = 0;
243
+
244
+ while (value >= 1024 && unitIndex < units.length - 1) {
245
+ value /= 1024;
246
+ unitIndex += 1;
247
+ }
248
+
249
+ const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
250
+ return `${new Intl.NumberFormat(locale || 'pt-BR', {
251
+ minimumFractionDigits: 0,
252
+ maximumFractionDigits: digits,
253
+ }).format(value)} ${units[unitIndex]}`;
254
+ };
255
+
256
+ const kpiItems = useMemo(() => {
257
+ if (!data) return [];
258
+ return [
259
+ {
260
+ key: 'modules',
261
+ title: t('kpis.modules'),
262
+ value: data.structure.moduleCount,
263
+ description: t('kpis.modulesDescription'),
264
+ icon: Layers,
265
+ accentClassName: 'from-blue-500 via-indigo-500 to-violet-500',
266
+ iconContainerClassName:
267
+ 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
268
+ },
269
+ {
270
+ key: 'lessons',
271
+ title: t('kpis.lessons'),
272
+ value: data.structure.lessonCount,
273
+ description: t('kpis.lessonsDescription'),
274
+ icon: BookOpen,
275
+ accentClassName: 'from-emerald-500 via-teal-500 to-green-500',
276
+ iconContainerClassName:
277
+ 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
278
+ },
279
+ {
280
+ key: 'images',
281
+ title: t('kpis.extractedImages'),
282
+ value: data.media.extractedImageCount,
283
+ description: t('kpis.extractedImagesDescription'),
284
+ icon: Image,
285
+ accentClassName: 'from-amber-500 via-orange-500 to-yellow-500',
286
+ iconContainerClassName:
287
+ 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
288
+ },
289
+ {
290
+ key: 'resources',
291
+ title: t('kpis.resourceFiles'),
292
+ value: data.resources.fileCount,
293
+ description: t('kpis.resourceFilesDescription'),
294
+ icon: Paperclip,
295
+ accentClassName: 'from-violet-500 via-fuchsia-500 to-purple-500',
296
+ iconContainerClassName:
297
+ 'bg-violet-500/10 text-violet-600 dark:text-violet-400',
298
+ },
299
+ ];
300
+ }, [data, t]);
301
+
302
+ const lessonTypeData = useMemo(() => {
303
+ if (!data) return [];
304
+ const types = ['video', 'questao', 'post'] as const;
305
+ return types
306
+ .map((type) => ({
307
+ type,
308
+ count: data.structure.lessonsByType[type],
309
+ color: TYPE_COLORS[type],
310
+ label: TYPE_GRADIENTS[type]!.label,
311
+ }))
312
+ .filter((item) => item.count > 0);
313
+ }, [data]);
314
+
315
+ const lessonTypeChartConfig = useMemo(
316
+ () =>
317
+ Object.fromEntries(
318
+ lessonTypeData.map((item) => [
319
+ item.type,
320
+ { label: item.label, color: item.color },
321
+ ])
322
+ ),
323
+ [lessonTypeData]
324
+ );
325
+
326
+ const storageCategories = useMemo(() => {
327
+ if (!data) return [];
328
+
329
+ return data.storage.categories.map((category) => {
330
+ const meta = (STORAGE_CATEGORY_META[category.key] ??
331
+ STORAGE_CATEGORY_META.other_lesson_file)!;
332
+ const sharePercent =
333
+ data.storage.totalBytes > 0
334
+ ? (category.totalBytes / data.storage.totalBytes) * 100
335
+ : 0;
336
+
337
+ return {
338
+ ...category,
339
+ ...meta,
340
+ sharePercent,
341
+ };
342
+ });
343
+ }, [data]);
344
+
345
+ useEffect(() => {
346
+ if (!cleanupTrackedJob || !cleanupTrackedJobDetail) return;
347
+
348
+ const status = cleanupTrackedJobDetail.status;
349
+
350
+ setCleanupTrackedJob((prev) => {
351
+ if (!prev) return prev;
352
+ if (prev.status === status) return prev;
353
+ return { ...prev, status };
354
+ });
355
+
356
+ if (status === 'completed') {
357
+ toast.success('Limpeza concluída com sucesso.');
358
+ void queryClient.invalidateQueries({
359
+ queryKey: ['course-content-overview', courseId],
360
+ });
361
+ setCleanupTrackedJob(null);
362
+ return;
363
+ }
364
+
365
+ if (['failed', 'dead_letter', 'canceled'].includes(status)) {
366
+ toast.error(
367
+ cleanupTrackedJobDetail.last_error ||
368
+ 'A limpeza falhou. Você pode tentar excluir novamente.'
369
+ );
370
+ setCleanupTrackedJob(null);
371
+ }
372
+ }, [cleanupTrackedJob, cleanupTrackedJobDetail, courseId, queryClient]);
373
+
374
+ useEffect(() => {
375
+ if (!cleanupTrackedJob || !isCleanupTrackedJobError) return;
376
+
377
+ toast.error('Não foi possível acompanhar o status da limpeza.');
378
+ setCleanupTrackedJob(null);
379
+ }, [cleanupTrackedJob, isCleanupTrackedJobError]);
380
+
381
+ if (isLoading) {
382
+ return (
383
+ <div className="flex flex-col gap-3">
384
+ <div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
385
+ {Array.from({ length: 4 }).map((_, i) => (
386
+ <Skeleton key={i} className="h-28 rounded-xl" />
387
+ ))}
388
+ </div>
389
+ <div className="grid gap-3 xl:grid-cols-2">
390
+ <Skeleton className="h-72 rounded-xl" />
391
+ <Skeleton className="h-72 rounded-xl" />
392
+ </div>
393
+ </div>
394
+ );
395
+ }
396
+
397
+ if (isError || !data) {
398
+ return (
399
+ <Card className="border-dashed bg-muted/20">
400
+ <CardHeader>
401
+ <CardTitle className="text-sm font-semibold">
402
+ {t('errorTitle')}
403
+ </CardTitle>
404
+ <CardDescription>{t('errorDescription')}</CardDescription>
405
+ </CardHeader>
406
+ </Card>
407
+ );
408
+ }
409
+
410
+ const totalLessons = data.structure.lessonCount;
411
+ const videoCount = data.videos.lessonCount;
412
+ const overviewHighlights = [
413
+ {
414
+ key: 'storage',
415
+ label: 'Armazenamento',
416
+ value: formatBytes(data.storage.totalBytes),
417
+ toneClassName:
418
+ 'border-cyan-500/15 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300',
419
+ },
420
+ {
421
+ key: 'coverage',
422
+ label: 'Cobertura de vídeo',
423
+ value:
424
+ totalLessons > 0
425
+ ? `${Math.round((videoCount / totalLessons) * 100)}% das aulas`
426
+ : '0% das aulas',
427
+ toneClassName:
428
+ 'border-teal-500/15 bg-teal-500/10 text-teal-700 dark:text-teal-300',
429
+ },
430
+ {
431
+ key: 'images',
432
+ label: 'Imagens extraídas',
433
+ value: `${data.media.extractedImageCount}`,
434
+ toneClassName:
435
+ 'border-amber-500/15 bg-amber-500/10 text-amber-700 dark:text-amber-300',
436
+ },
437
+ {
438
+ key: 'resources',
439
+ label: 'Arquivos de apoio',
440
+ value: `${data.resources.fileCount}`,
441
+ toneClassName:
442
+ 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300',
443
+ },
444
+ ];
445
+
446
+ const openCleanupCategoryDialog = (
447
+ categoryKey: string,
448
+ categoryLabel: string,
449
+ fileCount: number,
450
+ totalBytes: number
451
+ ) => {
452
+ if (fileCount <= 0 || cleanupCategoryKey) {
453
+ return;
454
+ }
455
+
456
+ setCleanupDialogState({
457
+ categoryKey,
458
+ categoryLabel,
459
+ fileCount,
460
+ totalBytes,
461
+ });
462
+ };
463
+
464
+ const handleCleanupCategory = async () => {
465
+ if (!cleanupDialogState) {
466
+ return;
467
+ }
468
+
469
+ const { categoryKey, categoryLabel } = cleanupDialogState;
470
+
471
+ setCleanupDialogState(null);
472
+
473
+ setCleanupCategoryKey(categoryKey);
474
+
475
+ try {
476
+ const response = await request<{
477
+ status?: string;
478
+ queueJobId?: number;
479
+ queueJobStatus?: string;
480
+ cleanupImpact?: { formattedSize?: string };
481
+ }>({
482
+ url: `/lms/courses/${courseId}/storage/cleanup`,
483
+ method: 'POST',
484
+ data: { category: categoryKey },
485
+ });
486
+
487
+ if (response.data?.status === 'empty') {
488
+ toast.info(
489
+ `A categoria "${categoryLabel}" não possui arquivos para limpeza.`
490
+ );
491
+ } else {
492
+ if (response.data?.queueJobId) {
493
+ setCleanupTrackedJob({
494
+ categoryKey,
495
+ queueJobId: response.data.queueJobId,
496
+ status: response.data.queueJobStatus || 'pending',
497
+ });
498
+ }
499
+ toast.success(
500
+ `Limpeza da categoria "${categoryLabel}" enfileirada com sucesso.`
501
+ );
502
+ }
503
+
504
+ await queryClient.invalidateQueries({
505
+ queryKey: ['course-content-overview', courseId],
506
+ });
507
+ } catch (error: unknown) {
508
+ const responseMessage =
509
+ typeof error === 'object' &&
510
+ error !== null &&
511
+ 'response' in error &&
512
+ typeof (error as { response?: { data?: { message?: unknown } } })
513
+ .response?.data?.message === 'string'
514
+ ? (error as { response?: { data?: { message?: string } } }).response
515
+ ?.data?.message
516
+ : undefined;
517
+ const message =
518
+ responseMessage ||
519
+ (error instanceof Error ? error.message : undefined) ||
520
+ 'Não foi possível enfileirar a limpeza da categoria.';
521
+ toast.error(String(message));
522
+ } finally {
523
+ setCleanupCategoryKey(null);
524
+ }
525
+ };
526
+
527
+ return (
528
+ <>
529
+ <AlertDialog
530
+ open={Boolean(cleanupDialogState)}
531
+ onOpenChange={(open) => {
532
+ if (!open && !cleanupCategoryKey) {
533
+ setCleanupDialogState(null);
534
+ }
535
+ }}
536
+ >
537
+ <AlertDialogContent>
538
+ <AlertDialogHeader>
539
+ <AlertDialogTitle>Confirmar exclusão da categoria</AlertDialogTitle>
540
+ <AlertDialogDescription>
541
+ {cleanupDialogState
542
+ ? `Confirma excluir todos os ${cleanupDialogState.fileCount} arquivo(s) da categoria "${cleanupDialogState.categoryLabel}" deste curso?`
543
+ : 'Confirme a exclusão dos arquivos desta categoria.'}
544
+ {cleanupDialogState
545
+ ? ` Espaço estimado a liberar: ${formatBytes(cleanupDialogState.totalBytes)}.`
546
+ : ''}{' '}
547
+ A remoção será processada em background via fila.
548
+ </AlertDialogDescription>
549
+ </AlertDialogHeader>
550
+ <AlertDialogFooter>
551
+ <AlertDialogCancel disabled={cleanupCategoryKey !== null}>
552
+ Cancelar
553
+ </AlertDialogCancel>
554
+ <AlertDialogAction
555
+ disabled={cleanupCategoryKey !== null}
556
+ onClick={(event) => {
557
+ event.preventDefault();
558
+ void handleCleanupCategory();
559
+ }}
560
+ className="bg-red-600 text-white hover:bg-red-700"
561
+ >
562
+ {cleanupCategoryKey !== null ? 'Enfileirando...' : 'Excluir'}
563
+ </AlertDialogAction>
564
+ </AlertDialogFooter>
565
+ </AlertDialogContent>
566
+ </AlertDialog>
567
+
568
+ <div className="flex flex-col gap-2.5">
569
+ <div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-2.5 py-2 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-3 sm:py-2.5">
570
+ <div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/50 pb-2">
571
+ <div className="min-w-0">
572
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-700/80 dark:text-cyan-300/80">
573
+ Course overview
574
+ </div>
575
+ <div className="mt-1 text-sm font-semibold text-foreground">
576
+ Panorama estrutural e de mídia do curso
577
+ </div>
578
+ <div className="mt-0.5 text-xs text-muted-foreground">
579
+ Visão consolidada de módulos, aulas, tipos de conteúdo, vídeo e
580
+ apoio.
581
+ </div>
582
+ </div>
583
+ <div className="flex flex-wrap gap-1.5">
584
+ {overviewHighlights.map((item) => (
585
+ <div
586
+ key={item.key}
587
+ className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md ${item.toneClassName}`}
588
+ >
589
+ <span className="text-[10px] font-semibold uppercase tracking-[0.14em] opacity-80">
590
+ {item.label}
591
+ </span>
592
+ <span className="font-semibold">{item.value}</span>
593
+ </div>
594
+ ))}
595
+ </div>
596
+ </div>
597
+
598
+ <div className="mt-2">
599
+ <KpiCardsGrid
600
+ items={kpiItems.map((item) => ({
601
+ ...item,
602
+ layout: 'compact' as const,
603
+ }))}
604
+ columns={4}
605
+ className="gap-2.5"
606
+ cardClassName="shadow-[0_12px_24px_-22px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_18px_34px_-22px_rgba(15,23,42,0.5)]"
607
+ />
608
+ </div>
609
+ </div>
610
+
611
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
612
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-cyan-500/12 via-sky-500/6 to-transparent pt-2.5 pb-1.5!">
613
+ <div className="flex flex-wrap items-start justify-between gap-3">
614
+ <div className="flex min-w-0 items-start gap-3">
615
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-cyan-500/10 text-cyan-600 ring-1 ring-inset ring-cyan-500/15 dark:text-cyan-400">
616
+ <HardDrive className="size-4" />
617
+ </div>
618
+ <div className="min-w-0">
619
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-600/80 dark:text-cyan-400/80">
620
+ Storage footprint
621
+ </div>
622
+ <CardTitle className="mt-1 text-sm font-semibold">
623
+ Uso de armazenamento do curso
624
+ </CardTitle>
625
+ <CardDescription className="mt-0.5">
626
+ Soma dos arquivos do conteúdo do curso, com detalhamento por
627
+ categoria.
628
+ </CardDescription>
629
+ </div>
630
+ </div>
631
+ <div className="rounded-full border border-cyan-500/15 bg-cyan-500/10 px-2.5 py-1 text-[10px] font-semibold text-cyan-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-cyan-500/15 hover:shadow-md dark:text-cyan-300">
632
+ {formatBytes(data.storage.totalBytes)}
633
+ </div>
634
+ </div>
635
+ </CardHeader>
636
+ <CardContent className="grid gap-3 px-2.5 py-2.5 sm:px-3 xl:grid-cols-[minmax(0,240px)_minmax(0,1fr)]">
637
+ <div className="grid gap-2">
638
+ <div className="rounded-2xl border border-cyan-500/15 bg-linear-to-br from-cyan-500/12 via-sky-500/6 to-transparent px-3 py-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
639
+ <div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-cyan-700/80 dark:text-cyan-300/80">
640
+ Total armazenado
641
+ </div>
642
+ <div className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
643
+ {formatBytes(data.storage.totalBytes)}
644
+ </div>
645
+ <div className="mt-1 text-xs text-muted-foreground">
646
+ {data.storage.totalFileCount} arquivos relacionados ao curso
647
+ </div>
648
+ </div>
649
+
650
+ <div className="grid grid-cols-2 gap-2">
651
+ <div className="rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:bg-background/95 hover:shadow-sm">
652
+ <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
653
+ Categorias
654
+ </div>
655
+ <div className="mt-1 text-lg font-semibold text-foreground">
656
+ {storageCategories.length}
657
+ </div>
658
+ </div>
659
+ <div className="rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:bg-background/95 hover:shadow-sm">
660
+ <div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
661
+ Maior bloco
662
+ </div>
663
+ <div className="mt-1 truncate text-sm font-semibold text-foreground">
664
+ {storageCategories[0]?.label ?? 'Nenhum'}
665
+ </div>
666
+ </div>
667
+ </div>
668
+ </div>
669
+
670
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-2">
671
+ {storageCategories.length > 0 ? (
672
+ storageCategories.map((category) => {
673
+ const Icon = category.icon;
674
+
675
+ return (
676
+ <div
677
+ key={category.key}
678
+ className="overflow-hidden rounded-2xl border border-border/60 bg-card/90 transition-all duration-200 hover:-translate-y-0.5 hover:border-cyan-500/20 hover:bg-background/95 hover:shadow-md"
679
+ >
680
+ <div
681
+ className={`h-1.5 w-full bg-linear-to-r ${category.accentClassName}`}
682
+ />
683
+ <div className="flex flex-col gap-3 px-3 py-3">
684
+ <div className="flex items-start gap-3">
685
+ <div
686
+ className={`flex size-9 shrink-0 items-center justify-center rounded-xl ${category.iconBackgroundClassName}`}
687
+ >
688
+ <Icon
689
+ className={`size-4 ${category.iconClassName}`}
690
+ />
691
+ </div>
692
+ <div className="min-w-0 flex-1">
693
+ <div className="text-sm font-semibold text-foreground">
694
+ {category.label}
695
+ </div>
696
+ <div className="mt-0.5 text-xs text-muted-foreground">
697
+ {category.fileCount}{' '}
698
+ {category.fileCount === 1
699
+ ? 'arquivo'
700
+ : 'arquivos'}
701
+ </div>
702
+ </div>
703
+ <div className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-semibold text-muted-foreground">
704
+ {category.sharePercent.toFixed(
705
+ category.sharePercent >= 10 ? 0 : 1
706
+ )}
707
+ %
708
+ </div>
709
+ </div>
710
+
711
+ <div className="flex items-center justify-between gap-2">
712
+ <span className="text-[11px] text-muted-foreground">
713
+ Limpar categoria para liberar espaço
714
+ </span>
715
+ {cleanupTrackedJob?.categoryKey === category.key &&
716
+ [
717
+ 'pending',
718
+ 'scheduled',
719
+ 'processing',
720
+ 'retrying',
721
+ ].includes(cleanupTrackedJob.status) ? (
722
+ <Badge
723
+ variant="secondary"
724
+ className="h-7 gap-1.5 rounded-lg border border-amber-500/20 bg-amber-500/10 px-2.5 text-[11px] font-semibold text-amber-700 dark:text-amber-300"
725
+ >
726
+ <Loader2 className="size-3 animate-spin" />
727
+ Carregando...
728
+ </Badge>
729
+ ) : (
730
+ <Button
731
+ type="button"
732
+ size="sm"
733
+ variant="outline"
734
+ className="h-7 gap-1.5 rounded-lg border-red-500/30 bg-red-500/5 px-2.5 text-[11px] font-semibold text-red-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-red-500/10 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200"
735
+ disabled={
736
+ cleanupCategoryKey !== null ||
737
+ category.fileCount <= 0
738
+ }
739
+ onClick={() =>
740
+ openCleanupCategoryDialog(
741
+ category.key,
742
+ category.label,
743
+ category.fileCount,
744
+ category.totalBytes
745
+ )
746
+ }
747
+ >
748
+ {cleanupCategoryKey === category.key ? (
749
+ <>
750
+ <Loader2 className="size-3 animate-spin" />
751
+ Enfileirando...
752
+ </>
753
+ ) : (
754
+ <>
755
+ <Trash2 className="size-3" />
756
+ Excluir
757
+ </>
758
+ )}
759
+ </Button>
760
+ )}
761
+ </div>
762
+
763
+ <div>
764
+ <div className="flex items-baseline justify-between gap-3">
765
+ <span className="text-lg font-semibold tracking-tight text-foreground">
766
+ {formatBytes(category.totalBytes)}
767
+ </span>
768
+ <span className="text-xs text-muted-foreground">
769
+ de {formatBytes(data.storage.totalBytes)}
770
+ </span>
771
+ </div>
772
+ <div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted/80">
773
+ <div
774
+ className="h-full rounded-full transition-all duration-700 ease-out"
775
+ style={{
776
+ width: `${Math.max(category.sharePercent, category.totalBytes > 0 ? 2 : 0)}%`,
777
+ background: `linear-gradient(90deg, rgba(34,211,238,0.9), rgba(14,165,233,0.75))`,
778
+ }}
779
+ />
780
+ </div>
781
+ </div>
782
+ </div>
783
+ </div>
784
+ );
785
+ })
786
+ ) : (
787
+ <div className="flex min-h-32 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-muted/15 px-4 py-6 text-center text-sm text-muted-foreground">
788
+ Nenhum arquivo relacionado ao curso foi encontrado.
789
+ </div>
790
+ )}
791
+ </div>
792
+ </CardContent>
793
+ </Card>
794
+
795
+ <div className="grid gap-3 xl:grid-cols-2">
796
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
797
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-sky-500/12 via-blue-500/6 to-transparent pt-2.5 pb-1.5!">
798
+ <div className="flex flex-wrap items-start justify-between gap-3">
799
+ <div className="flex min-w-0 items-start gap-3">
800
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-sky-500/10 text-sky-600 ring-1 ring-inset ring-sky-500/15 dark:text-sky-400">
801
+ <Layers className="size-4" />
802
+ </div>
803
+ <div className="min-w-0">
804
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-600/80 dark:text-sky-400/80">
805
+ Lesson mix
806
+ </div>
807
+ <CardTitle className="mt-1 text-sm font-semibold">
808
+ {t('lessonTypes.title')}
809
+ </CardTitle>
810
+ <CardDescription className="mt-0.5">
811
+ {t('lessonTypes.description')}
812
+ </CardDescription>
813
+ </div>
814
+ </div>
815
+ <div className="rounded-full border border-sky-500/15 bg-sky-500/10 px-2.5 py-1 text-[10px] font-semibold text-sky-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-sky-500/15 hover:shadow-md dark:text-sky-300">
816
+ {totalLessons} aulas
817
+ </div>
818
+ </div>
819
+ </CardHeader>
820
+ <CardContent className="grid gap-2.5 px-2.5 py-2.5 sm:px-3 md:grid-cols-[minmax(0,200px)_minmax(0,1fr)] md:items-start">
821
+ {lessonTypeData.length > 0 ? (
822
+ <ChartContainer
823
+ config={lessonTypeChartConfig}
824
+ className="h-48 w-full"
825
+ >
826
+ <PieChart>
827
+ <Pie
828
+ data={lessonTypeData}
829
+ dataKey="count"
830
+ nameKey="label"
831
+ innerRadius={52}
832
+ outerRadius={80}
833
+ paddingAngle={3}
834
+ animationDuration={900}
835
+ strokeWidth={3}
836
+ stroke="hsl(var(--background))"
837
+ >
838
+ {lessonTypeData.map((entry) => (
839
+ <Cell key={entry.type} fill={entry.color} />
840
+ ))}
841
+ </Pie>
842
+ <ChartTooltip
843
+ content={
844
+ <ChartTooltipContent
845
+ hideIndicator
846
+ formatter={(value, _name, item) => {
847
+ const payload = item?.payload as
848
+ | { label?: string; count?: number }
849
+ | undefined;
850
+ const pct =
851
+ totalLessons > 0
852
+ ? (
853
+ ((payload?.count ?? 0) / totalLessons) *
854
+ 100
855
+ ).toFixed(1)
856
+ : '0';
857
+ return (
858
+ <div className="flex min-w-36 flex-col gap-1 text-xs">
859
+ <span className="font-medium text-foreground">
860
+ {payload?.label}
861
+ </span>
862
+ <span className="text-muted-foreground">
863
+ {value} aulas · {pct}%
864
+ </span>
865
+ </div>
866
+ );
867
+ }}
868
+ />
869
+ }
870
+ />
871
+ </PieChart>
872
+ </ChartContainer>
873
+ ) : (
874
+ <div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
875
+ Nenhuma aula
876
+ </div>
877
+ )}
878
+
879
+ <div className="grid gap-2">
880
+ {lessonTypeData.map((item) => {
881
+ const Icon = TYPE_GRADIENTS[item.type]!.icon;
882
+ const pct =
883
+ totalLessons > 0
884
+ ? ((item.count / totalLessons) * 100).toFixed(1)
885
+ : '0';
886
+ return (
887
+ <div
888
+ key={item.type}
889
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-sky-500/20 hover:bg-background/95 hover:shadow-md"
890
+ >
891
+ <div
892
+ className="flex size-8 shrink-0 items-center justify-center rounded-lg"
893
+ style={{ backgroundColor: `${item.color}20` }}
894
+ >
895
+ <Icon
896
+ className="size-4"
897
+ style={{ color: item.color }}
898
+ />
899
+ </div>
900
+ <div className="min-w-0 flex-1">
901
+ <div className="flex items-center justify-between">
902
+ <span className="text-sm font-medium text-foreground">
903
+ {item.label}
904
+ </span>
905
+ <span className="font-mono text-xs text-muted-foreground transition-colors duration-200 group-hover:text-foreground">
906
+ {pct}%
907
+ </span>
908
+ </div>
909
+ <div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-muted">
910
+ <div
911
+ className="h-full rounded-full transition-all duration-700"
912
+ style={{
913
+ width: `${pct}%`,
914
+ backgroundColor: item.color,
915
+ }}
916
+ />
917
+ </div>
918
+ <div className="mt-1 text-xs text-muted-foreground">
919
+ {item.count} {item.count === 1 ? 'aula' : 'aulas'}
920
+ </div>
921
+ </div>
922
+ </div>
923
+ );
924
+ })}
925
+ </div>
926
+ </CardContent>
927
+ </Card>
928
+
929
+ {/* Video coverage */}
930
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
931
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/12 via-cyan-500/6 to-transparent pt-2.5 pb-1.5!">
932
+ <div className="flex flex-wrap items-start justify-between gap-3">
933
+ <div className="flex min-w-0 items-start gap-3">
934
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
935
+ <Clapperboard className="size-4" />
936
+ </div>
937
+ <div className="min-w-0">
938
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
939
+ Video coverage
940
+ </div>
941
+ <CardTitle className="mt-1 text-sm font-semibold">
942
+ {t('videos.title')}
943
+ </CardTitle>
944
+ <CardDescription className="mt-0.5">
945
+ {t('videos.description')}
946
+ </CardDescription>
947
+ </div>
948
+ </div>
949
+ <div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
950
+ {videoCount} vídeos
951
+ </div>
952
+ </div>
953
+ </CardHeader>
954
+ <CardContent className="flex flex-col gap-3 px-2.5 py-2.5 sm:px-3">
955
+ {(
956
+ [
957
+ {
958
+ key: 'total',
959
+ label: t('videos.total'),
960
+ value: videoCount,
961
+ total: videoCount,
962
+ color: '#0ea5e9',
963
+ bgColor: 'bg-sky-500/10',
964
+ textColor: 'text-sky-600 dark:text-sky-400',
965
+ icon: Clapperboard,
966
+ },
967
+ {
968
+ key: 'transcription',
969
+ label: t('videos.withTranscription'),
970
+ value: data.videos.withTranscription,
971
+ total: videoCount,
972
+ color: '#14b8a6',
973
+ bgColor: 'bg-teal-500/10',
974
+ textColor: 'text-teal-600 dark:text-teal-400',
975
+ icon: Mic,
976
+ },
977
+ {
978
+ key: 'xp',
979
+ label: t('videos.withXp'),
980
+ value: data.videos.withXp,
981
+ total: videoCount,
982
+ color: '#8b5cf6',
983
+ bgColor: 'bg-violet-500/10',
984
+ textColor: 'text-violet-600 dark:text-violet-400',
985
+ icon: Zap,
986
+ },
987
+ ] as const
988
+ ).map((stat) => {
989
+ const pct =
990
+ stat.total > 0
991
+ ? Math.round((stat.value / stat.total) * 100)
992
+ : stat.key === 'total'
993
+ ? 100
994
+ : 0;
995
+ const Icon = stat.icon;
996
+ return (
997
+ <div
998
+ key={stat.key}
999
+ className="flex flex-col gap-2 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/60 hover:bg-background/70 hover:shadow-sm"
1000
+ >
1001
+ <div className="flex items-center gap-3">
1002
+ <div
1003
+ className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${stat.bgColor}`}
1004
+ >
1005
+ <Icon className={`size-4 ${stat.textColor}`} />
1006
+ </div>
1007
+ <div className="flex flex-1 items-center justify-between">
1008
+ <span className="text-sm font-medium text-foreground">
1009
+ {stat.label}
1010
+ </span>
1011
+ <div className="flex items-center gap-2">
1012
+ <span className="font-mono text-xs text-muted-foreground">
1013
+ {stat.value}/{stat.total}
1014
+ </span>
1015
+ <span
1016
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1017
+ style={{
1018
+ backgroundColor: `${stat.color}20`,
1019
+ color: stat.color,
1020
+ }}
1021
+ >
1022
+ {pct}%
1023
+ </span>
1024
+ </div>
1025
+ </div>
1026
+ </div>
1027
+ <div className="ml-11 h-2 w-full overflow-hidden rounded-full bg-muted">
1028
+ <div
1029
+ className="h-full rounded-full transition-all duration-700 ease-out"
1030
+ style={{
1031
+ width: `${pct}%`,
1032
+ backgroundColor: stat.color,
1033
+ }}
1034
+ />
1035
+ </div>
1036
+ </div>
1037
+ );
1038
+ })}
1039
+
1040
+ {videoCount === 0 && (
1041
+ <p className="text-center text-sm text-muted-foreground">
1042
+ Nenhuma aula de vídeo neste curso
1043
+ </p>
1044
+ )}
1045
+ </CardContent>
1046
+ </Card>
1047
+ </div>
1048
+
1049
+ {/* Areas and Skills (conditional on XP data) */}
1050
+ {xpData && (xpData.areas.length > 0 || xpData.skills.length > 0) && (
1051
+ <div className="grid gap-3 xl:grid-cols-2">
1052
+ {xpData.areas.length > 0 && (
1053
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
1054
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/10 via-cyan-500/5 to-transparent pt-2.5 pb-1.5!">
1055
+ <div className="flex flex-wrap items-start justify-between gap-3">
1056
+ <div className="flex min-w-0 items-start gap-3">
1057
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
1058
+ <Sparkles className="size-4" />
1059
+ </div>
1060
+ <div className="min-w-0">
1061
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
1062
+ Macro areas
1063
+ </div>
1064
+ <CardTitle className="mt-1 text-sm font-semibold">
1065
+ {t('areas.title')}
1066
+ </CardTitle>
1067
+ <CardDescription className="mt-0.5">
1068
+ {t('areas.description')}
1069
+ </CardDescription>
1070
+ </div>
1071
+ </div>
1072
+ <div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
1073
+ {xpData.areas.length} áreas
1074
+ </div>
1075
+ </div>
1076
+ </CardHeader>
1077
+ <CardContent className="flex flex-col gap-2 px-2.5 py-2.5 sm:px-3">
1078
+ {xpData.areas.map((area, index) => (
1079
+ <div
1080
+ key={area.xpAreaId}
1081
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-teal-500/20 hover:bg-background/95 hover:shadow-md"
1082
+ >
1083
+ <div
1084
+ className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
1085
+ style={{
1086
+ backgroundColor:
1087
+ AREA_COLORS[index % AREA_COLORS.length],
1088
+ }}
1089
+ >
1090
+ {index + 1}
1091
+ </div>
1092
+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
1093
+ {area.name}
1094
+ </span>
1095
+ <span
1096
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1097
+ style={{
1098
+ backgroundColor: `${AREA_COLORS[index % AREA_COLORS.length]}20`,
1099
+ color: AREA_COLORS[index % AREA_COLORS.length],
1100
+ }}
1101
+ >
1102
+ {area.sharePercent.toFixed(1)}%
1103
+ </span>
1104
+ </div>
1105
+ ))}
1106
+ </CardContent>
1107
+ </Card>
1108
+ )}
1109
+
1110
+ {xpData.skills.length > 0 && (
1111
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
1112
+ <CardHeader className="border-b border-border/70 bg-linear-to-r from-orange-500/10 via-amber-500/5 to-transparent pt-2.5 pb-1.5!">
1113
+ <div className="flex flex-wrap items-start justify-between gap-3">
1114
+ <div className="flex min-w-0 items-start gap-3">
1115
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/15 dark:text-orange-400">
1116
+ <Target className="size-4" />
1117
+ </div>
1118
+ <div className="min-w-0">
1119
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-orange-600/80 dark:text-orange-400/80">
1120
+ Skills
1121
+ </div>
1122
+ <CardTitle className="mt-1 text-sm font-semibold">
1123
+ {t('skills.title')}
1124
+ </CardTitle>
1125
+ <CardDescription className="mt-0.5">
1126
+ {t('skills.description')}
1127
+ </CardDescription>
1128
+ </div>
1129
+ </div>
1130
+ <div className="rounded-full border border-orange-500/15 bg-orange-500/10 px-2.5 py-1 text-[10px] font-semibold text-orange-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-orange-500/15 hover:shadow-md dark:text-orange-300">
1131
+ {xpData.skills.length} skills
1132
+ </div>
1133
+ </div>
1134
+ </CardHeader>
1135
+ <CardContent className="flex flex-col gap-2 px-2.5 py-2.5 sm:px-3">
1136
+ {xpData.skills.map((skill, index) => (
1137
+ <div
1138
+ key={skill.xpSkillId}
1139
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-orange-500/20 hover:bg-background/95 hover:shadow-md"
1140
+ >
1141
+ <div
1142
+ className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
1143
+ style={{
1144
+ backgroundColor:
1145
+ SKILL_COLORS[index % SKILL_COLORS.length],
1146
+ }}
1147
+ >
1148
+ {index + 1}
1149
+ </div>
1150
+ <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
1151
+ {skill.name}
1152
+ </span>
1153
+ <span
1154
+ className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
1155
+ style={{
1156
+ backgroundColor: `${SKILL_COLORS[index % SKILL_COLORS.length]}20`,
1157
+ color: SKILL_COLORS[index % SKILL_COLORS.length],
1158
+ }}
1159
+ >
1160
+ {skill.sharePercent.toFixed(1)}%
1161
+ </span>
1162
+ </div>
1163
+ ))}
1164
+ </CardContent>
1165
+ </Card>
1166
+ )}
1167
+ </div>
1168
+ )}
1169
+ </div>
1170
+ </>
1171
+ );
1172
+ }