@hed-hog/lms 0.0.358 → 0.0.364

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +65 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -0
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
  6. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  7. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
  8. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
  9. package/dist/class-group/class-group.controller.d.ts +16 -16
  10. package/dist/class-group/class-group.service.d.ts +12 -12
  11. package/dist/course/course-audio-transcription.service.d.ts +3 -2
  12. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  13. package/dist/course/course-audio-transcription.service.js +49 -8
  14. package/dist/course/course-audio-transcription.service.js.map +1 -1
  15. package/dist/course/course-lesson.controller.d.ts +4 -0
  16. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  17. package/dist/course/course-lesson.controller.js +10 -0
  18. package/dist/course/course-lesson.controller.js.map +1 -1
  19. package/dist/course/course-structure.controller.d.ts +11 -4
  20. package/dist/course/course-structure.controller.d.ts.map +1 -1
  21. package/dist/course/course-structure.controller.js +14 -0
  22. package/dist/course/course-structure.controller.js.map +1 -1
  23. package/dist/course/course-structure.service.d.ts +15 -1
  24. package/dist/course/course-structure.service.d.ts.map +1 -1
  25. package/dist/course/course-structure.service.js +139 -4
  26. package/dist/course/course-structure.service.js.map +1 -1
  27. package/dist/course/course-video-conversion.service.d.ts +8 -0
  28. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  29. package/dist/course/course-video-conversion.service.js +87 -51
  30. package/dist/course/course-video-conversion.service.js.map +1 -1
  31. package/dist/course/course.controller.d.ts +73 -1
  32. package/dist/course/course.controller.d.ts.map +1 -1
  33. package/dist/course/course.controller.js +27 -3
  34. package/dist/course/course.controller.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +15 -3
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +108 -4
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +631 -30
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  44. package/dist/course/dto/cleanup-course-storage.dto.js +33 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  48. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +4 -0
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  52. package/dist/course/dto/create-course-bulk-job.dto.js +21 -0
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  54. package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
  55. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  56. package/dist/course/lms-bulk-upload-automation.service.js +443 -0
  57. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  58. package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
  59. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  60. package/dist/course/lms-bulk-upload-infra.service.js +277 -0
  61. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  62. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  63. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  64. package/dist/course/lms-bulk-upload.constants.js +7 -0
  65. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  66. package/dist/course/lms-bulk-upload.controller.d.ts +153 -0
  67. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
  68. package/dist/course/lms-bulk-upload.controller.js +129 -0
  69. package/dist/course/lms-bulk-upload.controller.js.map +1 -0
  70. package/dist/course/lms-bulk-upload.service.d.ts +181 -0
  71. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
  72. package/dist/course/lms-bulk-upload.service.js +754 -0
  73. package/dist/course/lms-bulk-upload.service.js.map +1 -0
  74. package/dist/course/lms-setting.controller.d.ts +7 -1
  75. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  76. package/dist/course/lms-setting.controller.js +26 -3
  77. package/dist/course/lms-setting.controller.js.map +1 -1
  78. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  79. package/dist/enterprise/enterprise.service.d.ts +20 -20
  80. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  81. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  82. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  83. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  84. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  85. package/dist/enterprise/training/training-student.service.d.ts +1 -1
  86. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  87. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  88. package/dist/evaluation/evaluation.service.d.ts +8 -8
  89. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  90. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  91. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  92. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  93. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  94. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  95. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  96. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  97. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  98. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  99. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  100. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  101. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  102. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  103. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  104. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  105. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  106. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  107. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  108. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +26 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +304 -0
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  113. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  114. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  115. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  116. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  117. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  118. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  119. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  120. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  121. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  122. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  123. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  124. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  126. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  127. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  128. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  129. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  130. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  131. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  132. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  133. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +17 -2
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  138. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/update-profile.dto.js +87 -0
  140. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  141. package/dist/platforma/platforma.controller.d.ts +94 -7
  142. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  143. package/dist/platforma/platforma.controller.js +85 -2
  144. package/dist/platforma/platforma.controller.js.map +1 -1
  145. package/dist/platforma/platforma.service.d.ts +27 -0
  146. package/dist/platforma/platforma.service.d.ts.map +1 -0
  147. package/dist/platforma/platforma.service.js +274 -0
  148. package/dist/platforma/platforma.service.js.map +1 -0
  149. package/dist/student-xp/student-xp.controller.d.ts +41 -0
  150. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  151. package/dist/student-xp/student-xp.controller.js +114 -0
  152. package/dist/student-xp/student-xp.controller.js.map +1 -0
  153. package/dist/student-xp/student-xp.module.d.ts +3 -0
  154. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  155. package/dist/student-xp/student-xp.module.js +25 -0
  156. package/dist/student-xp/student-xp.module.js.map +1 -0
  157. package/dist/student-xp/student-xp.service.d.ts +65 -0
  158. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  159. package/dist/student-xp/student-xp.service.js +197 -0
  160. package/dist/student-xp/student-xp.service.js.map +1 -0
  161. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  162. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  163. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  164. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  165. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  166. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  167. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  168. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  169. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  170. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  171. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  172. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  173. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  174. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  175. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  176. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  177. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  178. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  179. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  180. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  181. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  182. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  183. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  184. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  185. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  186. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  187. package/dist/xp-catalog/xp-area.controller.js +105 -0
  188. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  189. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  190. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  191. package/dist/xp-catalog/xp-area.service.js +168 -0
  192. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  193. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  194. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  195. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  196. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  197. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  198. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  199. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  200. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  201. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  202. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  203. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  204. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  205. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  206. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  207. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  208. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  209. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  210. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  211. package/dist/xp-catalog/xp-skill.service.js +174 -0
  212. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  213. package/hedhog/data/menu.yaml +101 -0
  214. package/hedhog/data/role.yaml +8 -0
  215. package/hedhog/data/route.yaml +547 -0
  216. package/hedhog/data/setting_group.yaml +33 -0
  217. package/hedhog/data/xp_area.yaml +164 -0
  218. package/hedhog/data/xp_learning_type.yaml +131 -0
  219. package/hedhog/data/xp_skill.yaml +1834 -0
  220. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  221. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  222. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1453 -0
  223. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  224. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  225. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  226. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  227. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  228. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +4 -4
  229. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  230. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1170 -0
  231. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  232. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  233. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  234. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  235. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +55 -2
  236. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
  237. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
  238. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  239. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  240. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  241. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +21 -1
  242. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  243. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  244. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  245. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +54 -0
  246. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
  247. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  248. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  249. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  250. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  251. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  252. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  253. package/hedhog/frontend/app/courses/page.tsx.ejs +400 -230
  254. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  255. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  256. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  257. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  258. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  259. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  260. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  261. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  262. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  263. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  264. package/hedhog/frontend/messages/en.json +386 -3
  265. package/hedhog/frontend/messages/pt.json +386 -3
  266. package/hedhog/table/lesson_xp_map.yaml +50 -0
  267. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  268. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  269. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  270. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  271. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  272. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  273. package/hedhog/table/student_area_xp.yaml +30 -0
  274. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  275. package/hedhog/table/student_skill_xp.yaml +30 -0
  276. package/hedhog/table/student_xp_event.yaml +34 -0
  277. package/hedhog/table/xp_area.yaml +39 -0
  278. package/hedhog/table/xp_learning_type.yaml +34 -0
  279. package/hedhog/table/xp_skill.yaml +39 -0
  280. package/package.json +9 -8
  281. package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -0
  282. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  283. package/src/course/course-audio-transcription.service.ts +58 -21
  284. package/src/course/course-lesson.controller.ts +6 -1
  285. package/src/course/course-structure.controller.ts +10 -0
  286. package/src/course/course-structure.service.ts +174 -1
  287. package/src/course/course-video-conversion.service.ts +113 -75
  288. package/src/course/course.controller.ts +22 -3
  289. package/src/course/course.module.ts +15 -3
  290. package/src/course/course.service.ts +847 -30
  291. package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
  292. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  293. package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
  294. package/src/course/lms-bulk-upload-automation.service.ts +560 -0
  295. package/src/course/lms-bulk-upload-infra.service.ts +327 -0
  296. package/src/course/lms-bulk-upload.constants.ts +5 -0
  297. package/src/course/lms-bulk-upload.controller.ts +103 -0
  298. package/src/course/lms-bulk-upload.service.ts +1029 -0
  299. package/src/course/lms-setting.controller.ts +28 -1
  300. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  301. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  302. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  303. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  304. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  305. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +396 -0
  306. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  307. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  308. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  309. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  310. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  311. package/src/lms.module.ts +17 -2
  312. package/src/platforma/dto/update-profile.dto.ts +59 -0
  313. package/src/platforma/platforma.controller.ts +57 -2
  314. package/src/platforma/platforma.service.ts +268 -0
  315. package/src/student-xp/student-xp.controller.ts +76 -0
  316. package/src/student-xp/student-xp.module.ts +12 -0
  317. package/src/student-xp/student-xp.service.ts +236 -0
  318. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  319. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  320. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  321. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  322. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  323. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  324. package/src/xp-catalog/xp-area.controller.ts +64 -0
  325. package/src/xp-catalog/xp-area.service.ts +196 -0
  326. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  327. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  328. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  329. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  330. package/src/xp-catalog/xp-skill.service.ts +205 -0
@@ -8,28 +8,15 @@ import {
8
8
  SearchBar,
9
9
  ViewModeToggle,
10
10
  } from '@/components/entity-list';
11
+ import { useNotifications } from '@/components/notification-bell';
11
12
  import { Badge } from '@/components/ui/badge';
12
13
  import { Button } from '@/components/ui/button';
13
14
  import { Card, CardContent } from '@/components/ui/card';
14
15
  import { Checkbox } from '@/components/ui/checkbox';
15
- import {
16
- Dialog,
17
- DialogContent,
18
- DialogDescription,
19
- DialogFooter,
20
- DialogHeader,
21
- DialogTitle,
22
- } from '@/components/ui/dialog';
23
- import {
24
- DropdownMenu,
25
- DropdownMenuContent,
26
- DropdownMenuItem,
27
- DropdownMenuSeparator,
28
- DropdownMenuTrigger,
29
- } from '@/components/ui/dropdown-menu';
30
16
  import { Input } from '@/components/ui/input';
31
17
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
32
18
  import { Label } from '@/components/ui/label';
19
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
33
20
  import { Separator } from '@/components/ui/separator';
34
21
  import {
35
22
  Sheet,
@@ -38,7 +25,6 @@ import {
38
25
  SheetHeader,
39
26
  SheetTitle,
40
27
  } from '@/components/ui/sheet';
41
- import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
42
28
  import { Skeleton } from '@/components/ui/skeleton';
43
29
  import {
44
30
  Table,
@@ -54,16 +40,12 @@ import { useApp, useQuery } from '@hed-hog/next-app-provider';
54
40
  import { zodResolver } from '@hookform/resolvers/zod';
55
41
  import { AnimatePresence, motion } from 'framer-motion';
56
42
  import {
57
- AlertTriangle,
58
43
  Archive,
59
44
  Award,
60
45
  BookOpen,
61
- Eye,
62
46
  Layers,
63
47
  Loader2,
64
- MoreHorizontal,
65
48
  Paperclip,
66
- Pencil,
67
49
  Plus,
68
50
  Star,
69
51
  Trash2,
@@ -83,6 +65,8 @@ import {
83
65
  DEFAULT_COURSE_FORM_VALUES,
84
66
  getCourseSheetSchema,
85
67
  } from '../_components/course-form-sheet';
68
+ import { CourseDeleteDialog } from './_components/CourseDeleteDialog';
69
+ import { CourseRowActions } from './_components/CourseRowActions';
86
70
 
87
71
  const API_COURSES_CACHE_KEY = 'lms:courses:api-cache';
88
72
 
@@ -169,6 +153,26 @@ type ApiCategoryList = {
169
153
  pageSize: number;
170
154
  };
171
155
 
156
+ type ApiCourseDeletionImpact = {
157
+ fileCount: number;
158
+ totalBytes: number;
159
+ formattedSize: string;
160
+ countsBySource?: Record<string, number>;
161
+ enrollmentCount?: number;
162
+ };
163
+
164
+ type ApiCourseDetail = ApiCourse & {
165
+ deletionImpact?: ApiCourseDeletionImpact;
166
+ };
167
+
168
+ type ApiQueuedDeleteResponse = {
169
+ success: boolean;
170
+ status: 'queued';
171
+ queueJobId: number;
172
+ notificationId: number;
173
+ deletionImpact?: ApiCourseDeletionImpact;
174
+ };
175
+
172
176
  type Locale = {
173
177
  id?: number;
174
178
  code: string;
@@ -177,6 +181,8 @@ type Locale = {
177
181
 
178
182
  type ViewMode = 'cards' | 'list';
179
183
 
184
+ // ── Helpers ───────────────────────────────────────────────────────────────────
185
+
180
186
  function normalizeEnumValue(value?: string | null) {
181
187
  return String(value ?? '')
182
188
  .trim()
@@ -185,52 +191,29 @@ function normalizeEnumValue(value?: string | null) {
185
191
  .toLowerCase();
186
192
  }
187
193
 
188
- function toPtLevel(level?: string | null): CourseSheetFormValues['nivel'] {
189
- const normalizedLevel = normalizeEnumValue(level);
190
-
191
- if (normalizedLevel === 'iniciante' || normalizedLevel === 'beginner') {
192
- return 'iniciante';
193
- }
194
- if (
195
- normalizedLevel === 'intermediario' ||
196
- normalizedLevel === 'intermediate'
197
- ) {
198
- return 'intermediario';
199
- }
200
- if (normalizedLevel === 'avancado' || normalizedLevel === 'advanced') {
201
- return 'avancado';
202
- }
203
-
194
+ function toPtLevel(level?: string | null): Curso['nivel'] {
195
+ const v = normalizeEnumValue(level);
196
+ if (v === 'iniciante' || v === 'beginner') return 'iniciante';
197
+ if (v === 'intermediario' || v === 'intermediate') return 'intermediario';
198
+ if (v === 'avancado' || v === 'advanced') return 'avancado';
204
199
  return 'iniciante';
205
200
  }
206
201
 
202
+ function toPtStatus(status?: string | null): Curso['status'] {
203
+ const v = normalizeEnumValue(status);
204
+ if (v === 'publicado' || v === 'published' || v === 'active' || v === 'ativo')
205
+ return 'publicado';
206
+ if (v === 'rascunho' || v === 'draft') return 'rascunho';
207
+ if (v === 'arquivado' || v === 'archived') return 'arquivado';
208
+ return 'rascunho';
209
+ }
210
+
207
211
  function toPtOfferingType(type?: string | null): Curso['tipoCurso'] {
208
212
  if (type === 'scheduled') return 'agendado';
209
213
  if (type === 'blended') return 'hibrido';
210
214
  return 'on_demand';
211
215
  }
212
216
 
213
- function toPtStatus(status?: string | null): CourseSheetFormValues['status'] {
214
- const normalizedStatus = normalizeEnumValue(status);
215
-
216
- if (
217
- normalizedStatus === 'publicado' ||
218
- normalizedStatus === 'ativo' ||
219
- normalizedStatus === 'active' ||
220
- normalizedStatus === 'published'
221
- ) {
222
- return 'publicado';
223
- }
224
- if (normalizedStatus === 'rascunho' || normalizedStatus === 'draft') {
225
- return 'rascunho';
226
- }
227
- if (normalizedStatus === 'arquivado' || normalizedStatus === 'archived') {
228
- return 'arquivado';
229
- }
230
-
231
- return 'rascunho';
232
- }
233
-
234
217
  function mapApiCourse(course: ApiCourse): Curso {
235
218
  return {
236
219
  id: course.id,
@@ -288,6 +271,24 @@ function getContrastColor(hex: string) {
288
271
  return luminance > 0.6 ? '#111827' : '#FFFFFF';
289
272
  }
290
273
 
274
+ function formatBytes(bytes: number) {
275
+ if (!Number.isFinite(bytes) || bytes <= 0) {
276
+ return '0 B';
277
+ }
278
+
279
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
280
+ let value = bytes;
281
+ let unitIndex = 0;
282
+
283
+ while (value >= 1024 && unitIndex < units.length - 1) {
284
+ value /= 1024;
285
+ unitIndex += 1;
286
+ }
287
+
288
+ const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
289
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
290
+ }
291
+
291
292
  // ── Constants ─────────────────────────────────────────────────────────────────
292
293
 
293
294
  const NIVEL_COLOR: Record<string, string> = {
@@ -340,8 +341,12 @@ export default function CursosPage() {
340
341
  // UI
341
342
  const [sheetOpen, setSheetOpen] = useState(false);
342
343
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
344
+ const [confirmationInput, setConfirmationInput] = useState('');
343
345
  const [createCategorySheetOpen, setCreateCategorySheetOpen] = useState(false);
344
346
  const [saving, setSaving] = useState(false);
347
+ const [archivingCourse, setArchivingCourse] = useState(false);
348
+ const [queuingDelete, setQueuingDelete] = useState(false);
349
+ const [loadingDeleteImpact, setLoadingDeleteImpact] = useState(false);
345
350
  const [creatingCategory, setCreatingCategory] = useState(false);
346
351
  const [newCategoryName, setNewCategoryName] = useState('');
347
352
  const [newCategorySlug, setNewCategorySlug] = useState('');
@@ -349,12 +354,23 @@ export default function CursosPage() {
349
354
 
350
355
  // Data
351
356
  const [cursoToDelete, setCursoToDelete] = useState<Curso | null>(null);
357
+ const [deleteImpact, setDeleteImpact] =
358
+ useState<ApiCourseDeletionImpact | null>(null);
352
359
  const [cachedCourseList, setCachedCourseList] =
353
360
  useState<ApiCourseList | null>(null);
354
361
 
355
362
  // Selection
356
363
  const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
357
364
 
365
+ // Job tracking
366
+ const [deletingCourseIds, setDeletingCourseIds] = useState<Set<number>>(
367
+ new Set()
368
+ );
369
+ const [notificationIdToCourseId, setNotificationIdToCourseId] = useState<
370
+ Map<number, number>
371
+ >(new Map());
372
+ const { notifications } = useNotifications();
373
+
358
374
  // Search/filter state (reactive)
359
375
  const [buscaInput, setBuscaInput] = useState('');
360
376
  const [filtroStatusInput, setFiltroStatusInput] = useState('todos');
@@ -428,6 +444,8 @@ export default function CursosPage() {
428
444
  });
429
445
  return response.data;
430
446
  },
447
+ refetchInterval: 30_000,
448
+ refetchIntervalInBackground: false,
431
449
  });
432
450
 
433
451
  useEffect(() => {
@@ -446,6 +464,23 @@ export default function CursosPage() {
446
464
  filtroCatInput,
447
465
  ]);
448
466
 
467
+ // Track active deletion jobs from notifications
468
+ useEffect(() => {
469
+ const activeNotifications = notifications.filter(
470
+ (n) => n.progress != null && n.progress < 100
471
+ );
472
+
473
+ const coursesInProgress = new Set<number>();
474
+ activeNotifications.forEach((notification) => {
475
+ const courseId = notificationIdToCourseId.get(notification.id);
476
+ if (courseId) {
477
+ coursesInProgress.add(courseId);
478
+ }
479
+ });
480
+
481
+ setDeletingCourseIds(coursesInProgress);
482
+ }, [notifications, notificationIdToCourseId]);
483
+
449
484
  const { data: statsData } = useQuery<ApiCourseStats>({
450
485
  queryKey: ['lms-courses-stats'],
451
486
  queryFn: async () => {
@@ -455,6 +490,8 @@ export default function CursosPage() {
455
490
  });
456
491
  return response.data;
457
492
  },
493
+ refetchInterval: 30_000,
494
+ refetchIntervalInBackground: false,
458
495
  });
459
496
 
460
497
  const { data: categoryListData, refetch: refetchCategoryOptions } =
@@ -553,15 +590,6 @@ export default function CursosPage() {
553
590
  () => cursos.filter((curso) => selectedIds.has(curso.id)),
554
591
  [cursos, selectedIds]
555
592
  );
556
- const selectedArchivedIds = useMemo(
557
- () =>
558
- selectedCourses
559
- .filter((curso) => curso.status === 'arquivado')
560
- .map((curso) => curso.id),
561
- [selectedCourses]
562
- );
563
- const hasSelectedNonArchived =
564
- selectedCourses.length > selectedArchivedIds.length;
565
593
  const totalPages = Math.max(1, effectiveCourseList?.lastPage ?? 1);
566
594
  const safePage = Math.min(currentPage, totalPages);
567
595
  function clearFilters() {
@@ -591,16 +619,63 @@ export default function CursosPage() {
591
619
  );
592
620
  }
593
621
 
594
- function openDeleteDialog(curso: Curso, e: React.MouseEvent) {
595
- e.stopPropagation();
622
+ const deleteConfirmMatches =
623
+ confirmationInput === cursoToDelete?.tituloComercial;
624
+ const deleteRequiresArchive = cursoToDelete?.status !== 'arquivado';
625
+ const canDeleteCourse =
626
+ Boolean(cursoToDelete) &&
627
+ !deleteRequiresArchive &&
628
+ deleteConfirmMatches &&
629
+ !loadingDeleteImpact &&
630
+ !queuingDelete &&
631
+ !archivingCourse &&
632
+ Boolean(deleteImpact);
633
+
634
+ async function loadDeleteImpact(cursoId: number) {
635
+ setLoadingDeleteImpact(true);
636
+ setDeleteImpact(null);
596
637
 
597
- if (curso.status !== 'arquivado') {
598
- toast.error(t('toasts.deleteOnlyArchived'));
599
- return;
638
+ try {
639
+ const response = await request<ApiCourseDetail>({
640
+ url: `/lms/courses/${cursoId}`,
641
+ method: 'GET',
642
+ });
643
+
644
+ const impact = response.data.deletionImpact;
645
+ if (impact) {
646
+ setDeleteImpact({
647
+ ...impact,
648
+ formattedSize:
649
+ impact.formattedSize || formatBytes(impact.totalBytes ?? 0),
650
+ });
651
+ }
652
+
653
+ setCursoToDelete((previous) =>
654
+ previous && previous.id === cursoId
655
+ ? {
656
+ ...previous,
657
+ status: toPtStatus(response.data.status),
658
+ alunosInscritos:
659
+ typeof impact?.enrollmentCount === 'number'
660
+ ? impact.enrollmentCount
661
+ : previous.alunosInscritos,
662
+ }
663
+ : previous
664
+ );
665
+ } catch {
666
+ toast.error(t('toasts.courseDeleteImpactLoadError'));
667
+ } finally {
668
+ setLoadingDeleteImpact(false);
600
669
  }
670
+ }
601
671
 
672
+ async function openDeleteDialog(curso: Curso, e?: React.MouseEvent) {
673
+ e?.stopPropagation();
674
+ setConfirmationInput('');
675
+ setDeleteImpact(null);
602
676
  setCursoToDelete(curso);
603
677
  setDeleteDialogOpen(true);
678
+ await loadDeleteImpact(curso.id);
604
679
  }
605
680
 
606
681
  // ── Selection ─────────────────────────────────────────────────────────────
@@ -630,6 +705,10 @@ export default function CursosPage() {
630
705
  router.push(`/lms/courses/${curso.id}`);
631
706
  }
632
707
 
708
+ function openCourseInNewTab(curso: Curso) {
709
+ window.open(`/lms/courses/${curso.id}`, '_blank', 'noopener,noreferrer');
710
+ }
711
+
633
712
  async function onSubmit(data: CourseSheetFormValues) {
634
713
  setSaving(true);
635
714
  try {
@@ -673,26 +752,101 @@ export default function CursosPage() {
673
752
 
674
753
  async function confirmDelete() {
675
754
  if (!cursoToDelete) return;
755
+ if (confirmationInput !== cursoToDelete.tituloComercial) {
756
+ toast.error(
757
+ t('form.validationErrors.confirmationMismatch') ||
758
+ 'Nome do curso não corresponde.'
759
+ );
760
+ return;
761
+ }
762
+
676
763
  if (cursoToDelete.status !== 'arquivado') {
677
764
  toast.error(t('toasts.deleteOnlyArchived'));
678
765
  return;
679
766
  }
680
767
 
681
- await request({
682
- url: `/lms/courses/${cursoToDelete.id}`,
683
- method: 'DELETE',
684
- });
685
- setSelectedIds((prev) => {
686
- const n = new Set(prev);
687
- n.delete(cursoToDelete.id);
688
- return n;
689
- });
690
- toast.success(
691
- t('toasts.courseRemoved', { title: cursoToDelete.tituloComercial })
692
- );
693
- setCursoToDelete(null);
694
- setDeleteDialogOpen(false);
695
- await refetchCourses();
768
+ if (!deleteImpact) {
769
+ toast.error(t('toasts.courseDeleteImpactLoadError'));
770
+ return;
771
+ }
772
+
773
+ setQueuingDelete(true);
774
+ try {
775
+ const response = await request<ApiQueuedDeleteResponse>({
776
+ url: `/lms/courses/${cursoToDelete.id}`,
777
+ method: 'DELETE',
778
+ });
779
+
780
+ // Track the deletion job with the course ID
781
+ const notifId = response.data?.notificationId ?? 0;
782
+ if (notifId > 0) {
783
+ setNotificationIdToCourseId((prev) => {
784
+ const next = new Map(prev);
785
+ next.set(notifId, cursoToDelete.id);
786
+ return next;
787
+ });
788
+ setDeletingCourseIds((prev) => {
789
+ const next = new Set(prev);
790
+ next.add(cursoToDelete.id);
791
+ return next;
792
+ });
793
+ }
794
+
795
+ setSelectedIds((prev) => {
796
+ const n = new Set(prev);
797
+ n.delete(cursoToDelete.id);
798
+ return n;
799
+ });
800
+ toast.success(
801
+ t('toasts.courseDeleteQueued', {
802
+ title: cursoToDelete.tituloComercial,
803
+ files:
804
+ response.data?.deletionImpact?.fileCount ?? deleteImpact.fileCount,
805
+ })
806
+ );
807
+ setCursoToDelete(null);
808
+ setDeleteImpact(null);
809
+ setDeleteDialogOpen(false);
810
+ setConfirmationInput('');
811
+ await refetchCourses();
812
+ } catch (error) {
813
+ const message =
814
+ error instanceof Error && error.message ? error.message : '';
815
+
816
+ if (message.includes('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED')) {
817
+ toast.error(t('toasts.deleteOnlyArchived'));
818
+ } else {
819
+ toast.error(t('toasts.courseDeleteQueueError'));
820
+ }
821
+ } finally {
822
+ setQueuingDelete(false);
823
+ }
824
+ }
825
+
826
+ async function archiveCourseForDelete() {
827
+ if (!cursoToDelete || cursoToDelete.status === 'arquivado') return;
828
+
829
+ setArchivingCourse(true);
830
+ try {
831
+ await request({
832
+ url: `/lms/courses/${cursoToDelete.id}`,
833
+ method: 'PATCH',
834
+ data: { status: 'archived' },
835
+ });
836
+
837
+ setCursoToDelete((previous) =>
838
+ previous ? { ...previous, status: 'arquivado' } : previous
839
+ );
840
+ toast.success(
841
+ t('toasts.courseArchived', { title: cursoToDelete.tituloComercial })
842
+ );
843
+ await refetchCourses();
844
+ await loadDeleteImpact(cursoToDelete.id);
845
+ } catch {
846
+ toast.error(t('toasts.courseArchiveError'));
847
+ } finally {
848
+ setArchivingCourse(false);
849
+ }
696
850
  }
697
851
 
698
852
  // ── KPI counts ────────────────────────────────────────────────────────────
@@ -993,10 +1147,21 @@ export default function CursosPage() {
993
1147
  variant="secondary"
994
1148
  className="gap-1.5"
995
1149
  onClick={() => {
996
- toast.info(
997
- t('toasts.coursesArchived', { count: selectedIds.size })
998
- );
999
- setSelectedIds(new Set());
1150
+ Promise.all(
1151
+ Array.from(selectedIds).map((id) =>
1152
+ request({
1153
+ url: `/lms/courses/${id}`,
1154
+ method: 'PATCH',
1155
+ data: { status: 'archived' },
1156
+ })
1157
+ )
1158
+ ).then(async () => {
1159
+ toast.success(
1160
+ t('toasts.coursesArchived', { count: selectedIds.size })
1161
+ );
1162
+ setSelectedIds(new Set());
1163
+ await refetchCourses();
1164
+ });
1000
1165
  }}
1001
1166
  >
1002
1167
  <Archive className="size-3.5" /> {t('bulkActions.archive')}
@@ -1006,28 +1171,7 @@ export default function CursosPage() {
1006
1171
  variant="secondary"
1007
1172
  className="gap-1.5 text-destructive hover:text-destructive"
1008
1173
  onClick={() => {
1009
- if (selectedArchivedIds.length === 0) {
1010
- toast.error(t('toasts.deleteOnlyArchived'));
1011
- return;
1012
- }
1013
-
1014
- if (hasSelectedNonArchived) {
1015
- toast.warning(t('toasts.bulkDeleteArchivedOnly'));
1016
- }
1017
-
1018
- Promise.all(
1019
- selectedArchivedIds.map((id) =>
1020
- request({ url: `/lms/courses/${id}`, method: 'DELETE' })
1021
- )
1022
- ).then(async () => {
1023
- toast.success(
1024
- t('toasts.coursesRemoved', {
1025
- count: selectedArchivedIds.length,
1026
- })
1027
- );
1028
- setSelectedIds(new Set());
1029
- await refetchCourses();
1030
- });
1174
+ toast.info(t('toasts.bulkDeleteUseSingleFlow'));
1031
1175
  }}
1032
1176
  >
1033
1177
  <Trash2 className="size-3.5" /> {t('bulkActions.delete')}
@@ -1082,6 +1226,12 @@ export default function CursosPage() {
1082
1226
  <TableHead>{t('table.headers.level')}</TableHead>
1083
1227
  <TableHead>{t('table.headers.status')}</TableHead>
1084
1228
  <TableHead>{t('table.headers.categories')}</TableHead>
1229
+ <TableHead className="text-right">
1230
+ {t('table.headers.sessions')}
1231
+ </TableHead>
1232
+ <TableHead className="text-right">
1233
+ {t('table.headers.lessons')}
1234
+ </TableHead>
1085
1235
  <TableHead className="text-right">
1086
1236
  {t('cards.studentsLabel')}
1087
1237
  </TableHead>
@@ -1109,6 +1259,12 @@ export default function CursosPage() {
1109
1259
  <TableCell>
1110
1260
  <Skeleton className="h-5 w-28 rounded-full" />
1111
1261
  </TableCell>
1262
+ <TableCell className="text-right">
1263
+ <Skeleton className="ml-auto h-4 w-8" />
1264
+ </TableCell>
1265
+ <TableCell className="text-right">
1266
+ <Skeleton className="ml-auto h-4 w-8" />
1267
+ </TableCell>
1112
1268
  <TableCell className="text-right">
1113
1269
  <Skeleton className="ml-auto h-4 w-12" />
1114
1270
  </TableCell>
@@ -1158,6 +1314,12 @@ export default function CursosPage() {
1158
1314
  onDoubleClick={() =>
1159
1315
  router.push(`/lms/courses/${curso.id}`)
1160
1316
  }
1317
+ onMouseDown={(e) => {
1318
+ if (e.button === 1) {
1319
+ e.preventDefault();
1320
+ openCourseInNewTab(curso);
1321
+ }
1322
+ }}
1161
1323
  title={t('cards.tooltip')}
1162
1324
  >
1163
1325
  <div
@@ -1186,57 +1348,29 @@ export default function CursosPage() {
1186
1348
  <div className="flex items-start gap-2.5">
1187
1349
  <CourseAvatar
1188
1350
  fileId={curso.logoFileId}
1189
- title={curso.nomeInterno}
1351
+ title={curso.tituloComercial}
1190
1352
  className="size-10 shrink-0 rounded-lg"
1191
1353
  iconSize="size-5"
1192
1354
  />
1193
1355
  <div className="min-w-0 flex-1">
1194
- <div className="flex items-start justify-between gap-1">
1195
- <h3 className="line-clamp-1 text-sm font-semibold leading-snug text-foreground">
1196
- {curso.nomeInterno}
1197
- </h3>
1198
- <DropdownMenu>
1199
- <DropdownMenuTrigger asChild>
1200
- <Button
1201
- variant="ghost"
1202
- size="icon"
1203
- className="-mr-1.5 -mt-0.5 size-7 shrink-0"
1204
- onClick={(e) => e.stopPropagation()}
1205
- aria-label={t('table.actions.label')}
1206
- >
1207
- <MoreHorizontal className="size-3.5" />
1208
- </Button>
1209
- </DropdownMenuTrigger>
1210
- <DropdownMenuContent
1211
- align="end"
1212
- className="w-48"
1213
- >
1214
- <DropdownMenuItem
1215
- onClick={(e) => {
1216
- e.stopPropagation();
1217
- goToCourseDetails(curso, e);
1218
- }}
1219
- >
1220
- <Eye className="mr-2 size-4" />{' '}
1221
- {t('table.actions.viewDetails')}
1222
- </DropdownMenuItem>
1223
- <DropdownMenuItem
1224
- onClick={(e) => goToCourseDetails(curso, e)}
1225
- >
1226
- <Pencil className="mr-2 size-4" />{' '}
1227
- {t('table.actions.edit')}
1228
- </DropdownMenuItem>
1229
- <DropdownMenuSeparator />
1230
- <DropdownMenuItem
1231
- disabled={curso.status !== 'arquivado'}
1232
- className="text-destructive focus:text-destructive"
1233
- onClick={(e) => openDeleteDialog(curso, e)}
1234
- >
1235
- <Trash2 className="mr-2 size-4" />{' '}
1236
- {t('table.actions.delete')}
1237
- </DropdownMenuItem>
1238
- </DropdownMenuContent>
1239
- </DropdownMenu>
1356
+ <div
1357
+ className="flex items-center justify-between gap-1"
1358
+ onClick={(e) => e.stopPropagation()}
1359
+ >
1360
+ <p className="truncate text-sm font-semibold leading-tight text-foreground">
1361
+ {curso.tituloComercial}
1362
+ </p>
1363
+ <CourseRowActions
1364
+ className="ml-auto -mr-1"
1365
+ onView={() => goToCourseDetails(curso)}
1366
+ onEdit={() => goToCourseDetails(curso)}
1367
+ onDelete={(event) =>
1368
+ void openDeleteDialog(curso, event)
1369
+ }
1370
+ viewLabel={t('table.actions.viewDetails')}
1371
+ editLabel={t('table.actions.edit')}
1372
+ deleteLabel={t('table.actions.delete')}
1373
+ />
1240
1374
  </div>
1241
1375
 
1242
1376
  <div className="mt-1.5 flex flex-wrap items-center gap-1">
@@ -1251,6 +1385,26 @@ export default function CursosPage() {
1251
1385
  >
1252
1386
  {getStatusLabel(curso)}
1253
1387
  </Badge>
1388
+ {deletingCourseIds.has(curso.id) && (
1389
+ <motion.div
1390
+ initial={{ opacity: 0, scale: 0.8 }}
1391
+ animate={{ opacity: 1, scale: 1 }}
1392
+ exit={{ opacity: 0, scale: 0.8 }}
1393
+ transition={{
1394
+ type: 'spring',
1395
+ stiffness: 300,
1396
+ damping: 30,
1397
+ }}
1398
+ >
1399
+ <Badge
1400
+ variant="secondary"
1401
+ className="gap-1 rounded-full px-2 py-0.5 text-[10px]"
1402
+ >
1403
+ <Loader2 className="size-2.5 animate-spin" />
1404
+ Deletando...
1405
+ </Badge>
1406
+ </motion.div>
1407
+ )}
1254
1408
  </div>
1255
1409
  </div>
1256
1410
  </div>
@@ -1296,6 +1450,12 @@ export default function CursosPage() {
1296
1450
  <TableHead>{t('table.headers.level')}</TableHead>
1297
1451
  <TableHead>{t('table.headers.status')}</TableHead>
1298
1452
  <TableHead>{t('table.headers.categories')}</TableHead>
1453
+ <TableHead className="text-right">
1454
+ {t('table.headers.sessions')}
1455
+ </TableHead>
1456
+ <TableHead className="text-right">
1457
+ {t('table.headers.lessons')}
1458
+ </TableHead>
1299
1459
  <TableHead className="text-right">
1300
1460
  {t('cards.studentsLabel')}
1301
1461
  </TableHead>
@@ -1316,6 +1476,12 @@ export default function CursosPage() {
1316
1476
  onDoubleClick={() =>
1317
1477
  router.push(`/lms/courses/${curso.id}`)
1318
1478
  }
1479
+ onMouseDown={(e) => {
1480
+ if (e.button === 1) {
1481
+ e.preventDefault();
1482
+ openCourseInNewTab(curso);
1483
+ }
1484
+ }}
1319
1485
  title={t('cards.tooltip')}
1320
1486
  >
1321
1487
  <TableCell onClick={(e) => e.stopPropagation()}>
@@ -1368,6 +1534,26 @@ export default function CursosPage() {
1368
1534
  >
1369
1535
  {getStatusLabel(curso)}
1370
1536
  </Badge>
1537
+ {deletingCourseIds.has(curso.id) && (
1538
+ <motion.div
1539
+ initial={{ opacity: 0, scale: 0.8 }}
1540
+ animate={{ opacity: 1, scale: 1 }}
1541
+ exit={{ opacity: 0, scale: 0.8 }}
1542
+ transition={{
1543
+ type: 'spring',
1544
+ stiffness: 300,
1545
+ damping: 30,
1546
+ }}
1547
+ >
1548
+ <Badge
1549
+ variant="secondary"
1550
+ className="gap-1 text-[11px]"
1551
+ >
1552
+ <Loader2 className="size-2.5 animate-spin" />
1553
+ Deletando...
1554
+ </Badge>
1555
+ </motion.div>
1556
+ )}
1371
1557
  {curso.destaque && (
1372
1558
  <span className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-700">
1373
1559
  <Star className="size-3 fill-amber-400 text-amber-400" />
@@ -1397,45 +1583,27 @@ export default function CursosPage() {
1397
1583
  )}
1398
1584
  </div>
1399
1585
  </TableCell>
1586
+ <TableCell className="text-right font-medium">
1587
+ {curso.quantidadeSessoes}
1588
+ </TableCell>
1589
+ <TableCell className="text-right font-medium">
1590
+ {curso.quantidadeAulas}
1591
+ </TableCell>
1400
1592
  <TableCell className="text-right font-medium">
1401
1593
  {curso.alunosInscritos.toLocaleString('pt-BR')}
1402
1594
  </TableCell>
1403
1595
  <TableCell onClick={(e) => e.stopPropagation()}>
1404
- <DropdownMenu>
1405
- <DropdownMenuTrigger asChild>
1406
- <Button
1407
- variant="ghost"
1408
- size="icon"
1409
- className="ml-auto size-8"
1410
- aria-label={t('table.actions.label')}
1411
- >
1412
- <MoreHorizontal className="size-4" />
1413
- </Button>
1414
- </DropdownMenuTrigger>
1415
- <DropdownMenuContent align="end" className="w-48">
1416
- <DropdownMenuItem
1417
- onClick={(e) => goToCourseDetails(curso, e)}
1418
- >
1419
- <Eye className="mr-2 size-4" />{' '}
1420
- {t('table.actions.viewDetails')}
1421
- </DropdownMenuItem>
1422
- <DropdownMenuItem
1423
- onClick={(e) => goToCourseDetails(curso, e)}
1424
- >
1425
- <Pencil className="mr-2 size-4" />{' '}
1426
- {t('table.actions.edit')}
1427
- </DropdownMenuItem>
1428
- <DropdownMenuSeparator />
1429
- <DropdownMenuItem
1430
- disabled={curso.status !== 'arquivado'}
1431
- className="text-destructive focus:text-destructive"
1432
- onClick={(e) => openDeleteDialog(curso, e)}
1433
- >
1434
- <Trash2 className="mr-2 size-4" />{' '}
1435
- {t('table.actions.delete')}
1436
- </DropdownMenuItem>
1437
- </DropdownMenuContent>
1438
- </DropdownMenu>
1596
+ <CourseRowActions
1597
+ className="ml-auto"
1598
+ onView={() => goToCourseDetails(curso)}
1599
+ onEdit={() => goToCourseDetails(curso)}
1600
+ onDelete={(event) =>
1601
+ void openDeleteDialog(curso, event)
1602
+ }
1603
+ viewLabel={t('table.actions.viewDetails')}
1604
+ editLabel={t('table.actions.edit')}
1605
+ deleteLabel={t('table.actions.delete')}
1606
+ />
1439
1607
  </TableCell>
1440
1608
  </TableRow>
1441
1609
  );
@@ -1559,42 +1727,44 @@ export default function CursosPage() {
1559
1727
  </Sheet>
1560
1728
 
1561
1729
  {/* ── Delete dialog ────────────────────���────────────────────────────── */}
1562
- <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
1563
- <DialogContent className="max-w-3xl">
1564
- <DialogHeader>
1565
- <DialogTitle className="flex items-center gap-2">
1566
- <AlertTriangle className="size-5 text-destructive" />
1567
- {t('deleteDialog.title')}
1568
- </DialogTitle>
1569
- <DialogDescription>
1570
- {t('deleteDialog.description')}{' '}
1571
- <strong>{cursoToDelete?.tituloComercial}</strong>?
1572
- {(cursoToDelete?.alunosInscritos ?? 0) > 0 && (
1573
- <span className="mt-2 block rounded-md bg-destructive/10 p-2 text-destructive text-sm">
1574
- {t('deleteDialog.warning', {
1575
- count: cursoToDelete?.alunosInscritos ?? 0,
1576
- })}
1577
- </span>
1578
- )}
1579
- </DialogDescription>
1580
- </DialogHeader>
1581
- <DialogFooter className="gap-2">
1582
- <Button
1583
- variant="outline"
1584
- onClick={() => setDeleteDialogOpen(false)}
1585
- >
1586
- {t('deleteDialog.actions.cancel')}
1587
- </Button>
1588
- <Button
1589
- variant="destructive"
1590
- onClick={confirmDelete}
1591
- className="gap-2"
1592
- >
1593
- <Trash2 className="size-4" /> {t('deleteDialog.actions.delete')}
1594
- </Button>
1595
- </DialogFooter>
1596
- </DialogContent>
1597
- </Dialog>
1730
+ <CourseDeleteDialog
1731
+ open={deleteDialogOpen}
1732
+ onOpenChange={(open) => {
1733
+ setDeleteDialogOpen(open);
1734
+ if (!open) {
1735
+ setConfirmationInput('');
1736
+ setDeleteImpact(null);
1737
+ setCursoToDelete(null);
1738
+ }
1739
+ }}
1740
+ course={
1741
+ cursoToDelete
1742
+ ? {
1743
+ title: cursoToDelete.tituloComercial,
1744
+ status: cursoToDelete.status as
1745
+ | 'publicado'
1746
+ | 'rascunho'
1747
+ | 'arquivado',
1748
+ enrollmentCount: cursoToDelete.alunosInscritos,
1749
+ }
1750
+ : null
1751
+ }
1752
+ impact={deleteImpact}
1753
+ loadingImpact={loadingDeleteImpact}
1754
+ confirmationInput={confirmationInput}
1755
+ onConfirmationInputChange={setConfirmationInput}
1756
+ archiving={archivingCourse}
1757
+ queuingDelete={queuingDelete}
1758
+ canDelete={canDeleteCourse}
1759
+ onArchive={() => void archiveCourseForDelete()}
1760
+ onDelete={() => void confirmDelete()}
1761
+ t={
1762
+ t as unknown as (
1763
+ key: string,
1764
+ values?: Record<string, string | number | Date>
1765
+ ) => string
1766
+ }
1767
+ />
1598
1768
  </Page>
1599
1769
  );
1600
1770
  }