@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,1749 @@
1
+ 'use client';
2
+
3
+ import { CopyButton } from '@/components/copy-button';
4
+ import {
5
+ EmptyState,
6
+ Page,
7
+ PageHeader,
8
+ PaginationFooter,
9
+ SearchBar,
10
+ } from '@/components/entity-list';
11
+ import { FileTypeIcon } from '@/components/file-type-icon';
12
+ import {
13
+ IntegrationProfileSheet,
14
+ type IntegrationProfileSheetSavedProfile,
15
+ } from '@/components/integration-profile/integration-profile-sheet';
16
+ import {
17
+ AlertDialog,
18
+ AlertDialogAction,
19
+ AlertDialogCancel,
20
+ AlertDialogContent,
21
+ AlertDialogDescription,
22
+ AlertDialogHeader,
23
+ AlertDialogTitle,
24
+ } from '@/components/ui/alert-dialog';
25
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
26
+ import { Badge } from '@/components/ui/badge';
27
+ import { Button } from '@/components/ui/button';
28
+ import { Checkbox } from '@/components/ui/checkbox';
29
+ import { EntityPicker } from '@/components/ui/entity-picker';
30
+ import { Input } from '@/components/ui/input';
31
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
32
+ import { Label } from '@/components/ui/label';
33
+ import { Progress } from '@/components/ui/progress';
34
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
35
+ import { ScrollArea } from '@/components/ui/scroll-area';
36
+ import {
37
+ Sheet,
38
+ SheetDescription,
39
+ SheetHeader,
40
+ SheetTitle,
41
+ } from '@/components/ui/sheet';
42
+ import { Skeleton } from '@/components/ui/skeleton';
43
+ import {
44
+ Table,
45
+ TableBody,
46
+ TableCell,
47
+ TableHead,
48
+ TableHeader,
49
+ TableRow,
50
+ } from '@/components/ui/table';
51
+ import {
52
+ Tooltip,
53
+ TooltipContent,
54
+ TooltipProvider,
55
+ TooltipTrigger,
56
+ } from '@/components/ui/tooltip';
57
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
58
+ import { getPhotoUrl } from '@/lib/get-photo-url';
59
+ import { cn } from '@/lib/utils';
60
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
61
+ import {
62
+ AlertCircle,
63
+ Ban,
64
+ CheckCircle2,
65
+ Clock,
66
+ Cog,
67
+ ExternalLink,
68
+ HardDriveUpload,
69
+ Link2,
70
+ Loader2,
71
+ Pencil,
72
+ Plus,
73
+ RefreshCw,
74
+ Save,
75
+ Trash2,
76
+ Webhook,
77
+ XCircle,
78
+ } from 'lucide-react';
79
+ import { useEffect, useMemo, useState } from 'react';
80
+ import { toast } from 'sonner';
81
+
82
+ type UploadStatus =
83
+ | 'queued'
84
+ | 'uploading'
85
+ | 'cancelling'
86
+ | 'received'
87
+ | 'done'
88
+ | 'error'
89
+ | 'cancelled'
90
+ | 'lesson_not_found';
91
+
92
+ type UploadItemRow = {
93
+ id: number;
94
+ sessionId: number;
95
+ uploadId: string;
96
+ fileName: string;
97
+ sizeBytes: number;
98
+ status: UploadStatus;
99
+ progressPercent: number;
100
+ errorMessage: string | null;
101
+ uploadedKey: string | null;
102
+ receivedAt: string | null;
103
+ updatedAt: string;
104
+ completedAt: string | null;
105
+ appName: string;
106
+ userId: number;
107
+ userPhotoId: number | null;
108
+ userName: string | null;
109
+ sessionStatus: string;
110
+ startedAt: string;
111
+ matchedCourseId: number | null;
112
+ matchedCourseTitle: string | null;
113
+ matchedCourseSlug: string | null;
114
+ matchedCourseLogoFileId: number | null;
115
+ matchedSessionId: number | null;
116
+ matchedSessionTitle: string | null;
117
+ matchedLessonId: number | null;
118
+ matchedLessonTitle: string | null;
119
+ };
120
+
121
+ type UploadListResponse = {
122
+ data: UploadItemRow[];
123
+ total: number;
124
+ page: number;
125
+ pageSize: number;
126
+ lastPage: number;
127
+ };
128
+
129
+ type Paginated<T> = {
130
+ data: T[];
131
+ total: number;
132
+ page: number;
133
+ pageSize: number;
134
+ lastPage: number;
135
+ prev?: number | null;
136
+ next?: number | null;
137
+ };
138
+
139
+ type BulkUploadSettingsResponse = {
140
+ storageProfileId: number | null;
141
+ bucketName: string;
142
+ lambdaRoleArn: string;
143
+ sessionDurationSeconds: number;
144
+ };
145
+
146
+ type IntegrationProfileOption = {
147
+ id: number;
148
+ name: string;
149
+ slug: string;
150
+ };
151
+
152
+ type WebhookIntegrationItem = {
153
+ id: number;
154
+ slug: string;
155
+ name: string;
156
+ status: 'active' | 'inactive';
157
+ require_token: boolean;
158
+ public_url: string | null;
159
+ plainToken?: string | null;
160
+ updated_at?: string | null;
161
+ };
162
+
163
+ type BulkUploadInfrastructureResponse = {
164
+ outputs?: {
165
+ bucket_arn?: string;
166
+ bucket_name?: string;
167
+ lambda_arn?: string;
168
+ lambda_function_name?: string;
169
+ } | null;
170
+ };
171
+
172
+ type BulkUploadConfigureResponse = {
173
+ success: boolean;
174
+ storageProfileId: number;
175
+ bucketName: string;
176
+ region: string;
177
+ credentials: {
178
+ expiresAt: string | null;
179
+ };
180
+ infrastructure?: BulkUploadInfrastructureResponse;
181
+ webhook: WebhookIntegrationItem;
182
+ };
183
+
184
+ type BulkUploadRegenerateTokenResponse = {
185
+ success: boolean;
186
+ storageProfileId: number;
187
+ bucketName: string;
188
+ region: string;
189
+ infrastructure?: BulkUploadInfrastructureResponse;
190
+ webhook: WebhookIntegrationItem;
191
+ };
192
+
193
+ type BulkUploadCleanupStatus = 'received' | 'done' | 'error' | 'cancelled';
194
+ type BulkUploadCleanupTimeWindow =
195
+ | 'last_hour'
196
+ | 'last_day'
197
+ | 'last_week'
198
+ | 'all_time';
199
+
200
+ type BulkUploadCleanupResponse = {
201
+ success: boolean;
202
+ deletedItems: number;
203
+ deletedSessions: number;
204
+ filtersApplied: {
205
+ statuses: BulkUploadCleanupStatus[];
206
+ timeWindow: BulkUploadCleanupTimeWindow;
207
+ };
208
+ };
209
+
210
+ const BULK_UPLOAD_STORAGE_PROFILE_SLUG = 'lms-bulk-upload-storage-profile-id';
211
+ const BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG = 'lms-bulk-upload-lambda-role-arn';
212
+ const BULK_UPLOAD_WEBHOOK_SLUG = 'lms-bulk-upload';
213
+
214
+ const PAGE_SIZE_OPTIONS = [6, 12, 24, 48] as const;
215
+ const CLEANUP_STATUS_OPTIONS: Array<{
216
+ value: BulkUploadCleanupStatus;
217
+ label: string;
218
+ }> = [
219
+ { value: 'received', label: 'Concluidos (webhook confirmado)' },
220
+ { value: 'done', label: 'Enviados (aguardando confirmação)' },
221
+ { value: 'error', label: 'Com falha' },
222
+ { value: 'cancelled', label: 'Cancelados' },
223
+ ];
224
+
225
+ const CLEANUP_TIME_WINDOW_OPTIONS: Array<{
226
+ value: BulkUploadCleanupTimeWindow;
227
+ label: string;
228
+ description: string;
229
+ }> = [
230
+ {
231
+ value: 'last_hour',
232
+ label: 'Ultima hora',
233
+ description: 'Remove itens atualizados na ultima hora.',
234
+ },
235
+ {
236
+ value: 'last_day',
237
+ label: 'Ultimo dia',
238
+ description: 'Remove itens atualizados nas ultimas 24 horas.',
239
+ },
240
+ {
241
+ value: 'last_week',
242
+ label: 'Ultima semana',
243
+ description: 'Remove itens atualizados nos ultimos 7 dias.',
244
+ },
245
+ {
246
+ value: 'all_time',
247
+ label: 'Todos de sempre',
248
+ description: 'Remove todo o historico conforme os status marcados.',
249
+ },
250
+ ];
251
+
252
+ function formatBytes(bytes: number) {
253
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
254
+
255
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
256
+ const exponent = Math.min(
257
+ Math.floor(Math.log(bytes) / Math.log(1024)),
258
+ units.length - 1
259
+ );
260
+ const value = bytes / 1024 ** exponent;
261
+
262
+ return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[exponent]}`;
263
+ }
264
+
265
+ function formatDate(value: string | null) {
266
+ if (!value) return '-';
267
+ const date = new Date(value);
268
+ if (Number.isNaN(date.getTime())) return '-';
269
+ return date.toLocaleString('pt-BR');
270
+ }
271
+
272
+ function getStatusMeta(status: UploadStatus) {
273
+ switch (status) {
274
+ case 'received':
275
+ return {
276
+ label: 'Concluido',
277
+ variant: 'default' as const,
278
+ icon: CheckCircle2,
279
+ iconClass: 'text-green-600',
280
+ animated: false,
281
+ progressClass: 'bg-green-500',
282
+ };
283
+ case 'done':
284
+ return {
285
+ label: 'Aguardando confirmação',
286
+ variant: 'secondary' as const,
287
+ icon: HardDriveUpload,
288
+ iconClass: 'text-blue-500 animate-pulse',
289
+ animated: true,
290
+ progressClass: 'bg-blue-400',
291
+ };
292
+ case 'uploading':
293
+ return {
294
+ label: 'Enviando',
295
+ variant: 'secondary' as const,
296
+ icon: Loader2,
297
+ iconClass: 'text-amber-500 animate-spin',
298
+ animated: true,
299
+ progressClass: 'bg-amber-500',
300
+ };
301
+ case 'queued':
302
+ return {
303
+ label: 'Na fila',
304
+ variant: 'outline' as const,
305
+ icon: Clock,
306
+ iconClass: 'text-blue-500 animate-pulse',
307
+ animated: true,
308
+ progressClass: 'bg-blue-400',
309
+ };
310
+ case 'cancelling':
311
+ return {
312
+ label: 'Cancelando',
313
+ variant: 'secondary' as const,
314
+ icon: Loader2,
315
+ iconClass: 'text-orange-500 animate-spin',
316
+ animated: true,
317
+ progressClass: 'bg-orange-400',
318
+ };
319
+ case 'cancelled':
320
+ return {
321
+ label: 'Cancelado',
322
+ variant: 'outline' as const,
323
+ icon: Ban,
324
+ iconClass: 'text-muted-foreground',
325
+ animated: false,
326
+ progressClass: 'bg-muted-foreground/40',
327
+ };
328
+ case 'lesson_not_found':
329
+ return {
330
+ label: 'Aula não encontrada',
331
+ variant: 'outline' as const,
332
+ icon: AlertCircle,
333
+ iconClass: 'text-amber-500',
334
+ animated: false,
335
+ progressClass: 'bg-amber-400',
336
+ };
337
+ default:
338
+ return {
339
+ label: 'Erro',
340
+ variant: 'destructive' as const,
341
+ icon: XCircle,
342
+ iconClass: 'text-destructive animate-pulse',
343
+ animated: true,
344
+ progressClass: 'bg-destructive',
345
+ };
346
+ }
347
+ }
348
+
349
+ export default function LmsBulkUploadSessionsPage() {
350
+ const { request } = useApp();
351
+ const [page, setPage] = useState(1);
352
+ const [searchInput, setSearchInput] = useState('');
353
+ const [search, setSearch] = useState('');
354
+ const [statusFilter, setStatusFilter] = useState('all');
355
+ const [settingsOpen, setSettingsOpen] = useState(false);
356
+ const [bucketName, setBucketName] = useState('');
357
+ const [awsAccountId, setAwsAccountId] = useState('');
358
+ const [lambdaRoleName, setLambdaRoleName] = useState('');
359
+ const [storageProfileId, setStorageProfileId] = useState<number | null>(null);
360
+ const [webhookPlainToken, setWebhookPlainToken] = useState<string | null>(
361
+ null
362
+ );
363
+ const [webhookPreview, setWebhookPreview] =
364
+ useState<WebhookIntegrationItem | null>(null);
365
+ const [isSavingSettings, setIsSavingSettings] = useState(false);
366
+ const [isRegeneratingToken, setIsRegeneratingToken] = useState(false);
367
+ const [lastSetupFeedback, setLastSetupFeedback] = useState<{
368
+ lambdaName: string;
369
+ configuredAt: string;
370
+ } | null>(null);
371
+ const [profileRefreshToken, setProfileRefreshToken] = useState(0);
372
+ const [isProfileSheetOpen, setIsProfileSheetOpen] = useState(false);
373
+ const [editingProfileId, setEditingProfileId] = useState<number | null>(null);
374
+ const [isCleanupDialogOpen, setIsCleanupDialogOpen] = useState(false);
375
+ const [cleanupStatuses, setCleanupStatuses] = useState<
376
+ Set<BulkUploadCleanupStatus>
377
+ >(new Set(['received']));
378
+ const [cleanupTimeWindow, setCleanupTimeWindow] =
379
+ useState<BulkUploadCleanupTimeWindow>('last_day');
380
+ const [isCleaningHistory, setIsCleaningHistory] = useState(false);
381
+ const [manualLinkOpen, setManualLinkOpen] = useState(false);
382
+ const [manualLinkItemId, setManualLinkItemId] = useState<number | null>(null);
383
+ const [manualLinkVideoUrl, setManualLinkVideoUrl] = useState<string | null>(null);
384
+ const [manualLinkVideoLoading, setManualLinkVideoLoading] = useState(false);
385
+ const [manualLinkCourseId, setManualLinkCourseId] = useState<number | null>(null);
386
+ const [manualLinkSessionId, setManualLinkSessionId] = useState<number | null>(null);
387
+ const [manualLinkLessonId, setManualLinkLessonId] = useState<number | null>(null);
388
+ const [manualLinkStructure, setManualLinkStructure] = useState<{
389
+ sessions: Array<{
390
+ id: number;
391
+ title: string;
392
+ lessons: Array<{ id: number; title: string }>;
393
+ }>;
394
+ } | null>(null);
395
+ const [manualLinkStructureLoading, setManualLinkStructureLoading] = useState(false);
396
+ const [isLinkingLesson, setIsLinkingLesson] = useState(false);
397
+ const [pageSize, setPageSize] = usePersistedPageSize({
398
+ storageKey: 'pagination:global:pageSize',
399
+ defaultValue: PAGE_SIZE_OPTIONS[1],
400
+ allowedValues: PAGE_SIZE_OPTIONS,
401
+ });
402
+
403
+ const {
404
+ data: settingsResult,
405
+ isLoading: settingsLoading,
406
+ refetch: refetchSettings,
407
+ } = useQuery<BulkUploadSettingsResponse>({
408
+ queryKey: ['lms-bulk-upload-settings', settingsOpen],
409
+ enabled: settingsOpen,
410
+ queryFn: async () => {
411
+ const response = await request<BulkUploadSettingsResponse>({
412
+ url: '/lms/bulk-upload/settings',
413
+ method: 'GET',
414
+ });
415
+ return response.data;
416
+ },
417
+ });
418
+
419
+ const {
420
+ data: profilesResult,
421
+ isLoading: profilesLoading,
422
+ refetch: refetchProfiles,
423
+ } = useQuery<Paginated<IntegrationProfileOption>>({
424
+ queryKey: ['integration-storage-profiles', profileRefreshToken],
425
+ enabled: settingsOpen,
426
+ queryFn: async () => {
427
+ const response = await request<Paginated<IntegrationProfileOption>>({
428
+ url: '/integration-profile?typeSlug=storage&pageSize=100',
429
+ method: 'GET',
430
+ });
431
+ return response.data;
432
+ },
433
+ });
434
+
435
+ const { data: webhooksResult, refetch: refetchWebhooks } = useQuery<
436
+ Paginated<WebhookIntegrationItem>
437
+ >({
438
+ queryKey: ['lms-bulk-upload-webhook', settingsOpen],
439
+ enabled: settingsOpen,
440
+ queryFn: async () => {
441
+ const response = await request<Paginated<WebhookIntegrationItem>>({
442
+ url: `/webhook-integration?page=1&pageSize=100&search=${BULK_UPLOAD_WEBHOOK_SLUG}`,
443
+ method: 'GET',
444
+ });
445
+ return response.data;
446
+ },
447
+ });
448
+
449
+ const ACTIVE_STATUSES: UploadStatus[] = ['queued', 'uploading', 'cancelling', 'done'];
450
+
451
+ const {
452
+ data,
453
+ isLoading,
454
+ refetch: refetchSessions,
455
+ } = useQuery<UploadListResponse>({
456
+ queryKey: [
457
+ 'lms-bulk-upload-sessions',
458
+ page,
459
+ pageSize,
460
+ search,
461
+ statusFilter,
462
+ ],
463
+ queryFn: async () => {
464
+ const response = await request<UploadListResponse>({
465
+ url: '/lms/bulk-upload/sessions',
466
+ method: 'GET',
467
+ params: {
468
+ page,
469
+ pageSize,
470
+ search: search.trim() || undefined,
471
+ status: statusFilter === 'all' ? undefined : statusFilter,
472
+ },
473
+ });
474
+ return response.data;
475
+ },
476
+ refetchInterval: (query) => {
477
+ const currentRows = query.state.data?.data ?? [];
478
+ const hasActive = currentRows.some((row) => ACTIVE_STATUSES.includes(row.status));
479
+ return hasActive ? 10_000 : false;
480
+ },
481
+ });
482
+
483
+ const { data: coursesResult } = useQuery<{ data: Array<{ id: number; title: string; code: string | null }> }>({
484
+ queryKey: ['lms-bulk-upload-courses-picker', manualLinkOpen],
485
+ enabled: manualLinkOpen,
486
+ queryFn: async () => {
487
+ const response = await request<{ data: Array<{ id: number; title: string; code: string | null }> }>({
488
+ url: '/course?pageSize=500',
489
+ method: 'GET',
490
+ });
491
+ return response.data;
492
+ },
493
+ });
494
+
495
+ const rows = useMemo(() => data?.data ?? [], [data?.data]);
496
+ const total = data?.total ?? 0;
497
+ const stats = useMemo(
498
+ () => ({
499
+ sending: rows.filter((row) => row.status === 'uploading').length,
500
+ done: rows.filter((row) => row.status === 'received').length,
501
+ error: rows.filter((row) => row.status === 'error').length,
502
+ }),
503
+ [rows]
504
+ );
505
+
506
+ const kpiItems = useMemo(
507
+ () => [
508
+ {
509
+ key: 'sending',
510
+ title: 'Enviando',
511
+ value: isLoading ? '-' : stats.sending,
512
+ icon: Loader2,
513
+ accentClassName: 'from-amber-500/20 via-amber-500/10 to-transparent',
514
+ iconContainerClassName: 'bg-amber-500/10 text-amber-700',
515
+ layout: 'compact' as const,
516
+ },
517
+ {
518
+ key: 'done',
519
+ title: 'Concluidos',
520
+ value: isLoading ? '-' : stats.done,
521
+ icon: CheckCircle2,
522
+ accentClassName: 'from-green-500/20 via-green-500/10 to-transparent',
523
+ iconContainerClassName: 'bg-green-500/10 text-green-700',
524
+ layout: 'compact' as const,
525
+ },
526
+ {
527
+ key: 'error',
528
+ title: 'Erros',
529
+ value: isLoading ? '-' : stats.error,
530
+ icon: XCircle,
531
+ accentClassName: 'from-red-500/20 via-red-500/10 to-transparent',
532
+ iconContainerClassName: 'bg-red-500/10 text-red-700',
533
+ layout: 'compact' as const,
534
+ },
535
+ ],
536
+ [isLoading, stats]
537
+ );
538
+
539
+ const storageProfileOptions = useMemo(
540
+ () => profilesResult?.data ?? [],
541
+ [profilesResult?.data]
542
+ );
543
+
544
+ const selectedStorageProfile = useMemo(
545
+ () =>
546
+ storageProfileOptions.find(
547
+ (profile) => profile.id === storageProfileId
548
+ ) ?? null,
549
+ [storageProfileId, storageProfileOptions]
550
+ );
551
+
552
+ const webhookIntegration = useMemo(
553
+ () =>
554
+ (webhooksResult?.data ?? []).find(
555
+ (item) =>
556
+ item.slug === BULK_UPLOAD_WEBHOOK_SLUG ||
557
+ item.slug.startsWith(`${BULK_UPLOAD_WEBHOOK_SLUG}-`)
558
+ ) ??
559
+ webhookPreview ??
560
+ null,
561
+ [webhookPreview, webhooksResult?.data]
562
+ );
563
+
564
+ const lambdaRoleArn =
565
+ awsAccountId && lambdaRoleName
566
+ ? `arn:aws:iam::${awsAccountId}:role/${lambdaRoleName}`
567
+ : '';
568
+ const isAccountIdValid = !awsAccountId || /^\d{12}$/.test(awsAccountId);
569
+ const isRoleNameValid =
570
+ !lambdaRoleName || /^[a-zA-Z_0-9+=,.@\-_/]+$/.test(lambdaRoleName);
571
+ const hasBaseConfig = Boolean(
572
+ storageProfileId && awsAccountId && lambdaRoleName && isAccountIdValid && isRoleNameValid
573
+ );
574
+ const isWebhookActive = Boolean(
575
+ webhookIntegration?.status === 'active' && webhookIntegration?.public_url
576
+ );
577
+ const isFullyConfigured =
578
+ Boolean(lastSetupFeedback) || (hasBaseConfig && isWebhookActive);
579
+
580
+ const currentStatusLabel =
581
+ isSavingSettings || isRegeneratingToken
582
+ ? 'Validando e sincronizando...'
583
+ : isFullyConfigured
584
+ ? 'Configurado e funcionando'
585
+ : hasBaseConfig
586
+ ? 'Pronto para sincronizar'
587
+ : 'Configuração incompleta';
588
+
589
+ const currentStatusDescription =
590
+ isSavingSettings || isRegeneratingToken
591
+ ? 'Aguarde a conclusão do provisionamento da integração.'
592
+ : isFullyConfigured
593
+ ? 'Bucket, webhook e Lambda estão ativos e alinhados.'
594
+ : hasBaseConfig
595
+ ? 'Falta validar/sincronizar para concluir a operação.'
596
+ : 'Selecione o perfil de integração para iniciar a configuração.';
597
+
598
+ useEffect(() => {
599
+ setBucketName(String(settingsResult?.bucketName ?? '').trim());
600
+ const arn = String(settingsResult?.lambdaRoleArn ?? '').trim();
601
+ const arnMatch = arn.match(/^arn:[^:]*:iam::(\d{12}):role\/(.+)$/);
602
+ setAwsAccountId(arnMatch?.[1] ?? '');
603
+ setLambdaRoleName(arnMatch?.[2] ?? '');
604
+ const parsedProfileId = Number(settingsResult?.storageProfileId ?? 0);
605
+ setStorageProfileId(
606
+ Number.isFinite(parsedProfileId) && parsedProfileId > 0
607
+ ? parsedProfileId
608
+ : null
609
+ );
610
+ }, [
611
+ settingsResult?.bucketName,
612
+ settingsResult?.lambdaRoleArn,
613
+ settingsResult?.storageProfileId,
614
+ ]);
615
+
616
+ const saveBulkUploadSettings = async () => {
617
+ if (!storageProfileId || !lambdaRoleArn.trim()) {
618
+ return;
619
+ }
620
+
621
+ try {
622
+ setIsSavingSettings(true);
623
+ const response = await request<BulkUploadConfigureResponse>({
624
+ url: '/lms/bulk-upload/verify',
625
+ method: 'POST',
626
+ data: {
627
+ configure: true,
628
+ storageProfileId,
629
+ lambdaRoleArn: lambdaRoleArn.trim(),
630
+ },
631
+ });
632
+
633
+ setWebhookPreview(response.data.webhook ?? null);
634
+ setWebhookPlainToken(response.data.webhook?.plainToken ?? null);
635
+
636
+ const lambdaName =
637
+ response.data.infrastructure?.outputs?.lambda_function_name?.trim() ||
638
+ 'Lambda não identificada';
639
+
640
+ const configuredAtRaw =
641
+ response.data.webhook?.updated_at ?? new Date().toISOString();
642
+ const configuredAt = formatDate(configuredAtRaw);
643
+
644
+ setLastSetupFeedback({
645
+ lambdaName,
646
+ configuredAt,
647
+ });
648
+
649
+ toast.success('Tudo pronto!', {
650
+ description: `Lambda: ${lambdaName} • Configurado em: ${configuredAt}`,
651
+ });
652
+ await Promise.all([
653
+ refetchSettings(),
654
+ refetchProfiles(),
655
+ refetchWebhooks(),
656
+ ]);
657
+ } catch (error: unknown) {
658
+ const message =
659
+ (error as { response?: { data?: { message?: string } } })?.response?.data
660
+ ?.message ?? null;
661
+ toast.error('Falha ao salvar a configuração.', {
662
+ description: message || 'Verifique o ARN da role e as credenciais do perfil de storage.',
663
+ });
664
+ } finally {
665
+ setIsSavingSettings(false);
666
+ }
667
+ };
668
+
669
+ const openManualLink = async (itemId: number) => {
670
+ setManualLinkItemId(itemId);
671
+ setManualLinkCourseId(null);
672
+ setManualLinkSessionId(null);
673
+ setManualLinkLessonId(null);
674
+ setManualLinkStructure(null);
675
+ setManualLinkVideoUrl(null);
676
+ setManualLinkOpen(true);
677
+ setManualLinkVideoLoading(true);
678
+ try {
679
+ const res = await request<{ url: string }>({
680
+ url: `/lms/bulk-upload/item/${itemId}/video-url`,
681
+ method: 'GET',
682
+ });
683
+ setManualLinkVideoUrl(res.data.url);
684
+ } catch {
685
+ // vídeo indisponível, mas o sheet ainda abre
686
+ } finally {
687
+ setManualLinkVideoLoading(false);
688
+ }
689
+ };
690
+
691
+ const loadCourseStructure = async (courseId: number) => {
692
+ setManualLinkStructureLoading(true);
693
+ setManualLinkSessionId(null);
694
+ setManualLinkLessonId(null);
695
+ try {
696
+ const res = await request<{ sessions: Array<{ id: number; title: string; lessons: Array<{ id: number; title: string }> }> }>({
697
+ url: `/course/${courseId}/structure`,
698
+ method: 'GET',
699
+ });
700
+ setManualLinkStructure({ sessions: res.data.sessions ?? [] });
701
+ } catch {
702
+ setManualLinkStructure(null);
703
+ } finally {
704
+ setManualLinkStructureLoading(false);
705
+ }
706
+ };
707
+
708
+ const confirmManualLink = async () => {
709
+ if (!manualLinkItemId || !manualLinkCourseId || !manualLinkSessionId || !manualLinkLessonId) return;
710
+ try {
711
+ setIsLinkingLesson(true);
712
+ await request({
713
+ url: `/lms/bulk-upload/item/${manualLinkItemId}/link-lesson`,
714
+ method: 'POST',
715
+ data: {
716
+ courseId: manualLinkCourseId,
717
+ sessionId: manualLinkSessionId,
718
+ lessonId: manualLinkLessonId,
719
+ },
720
+ });
721
+ toast.success('Aula vinculada! O processamento do vídeo foi iniciado.');
722
+ setManualLinkOpen(false);
723
+ await refetchSessions();
724
+ } catch (error: unknown) {
725
+ const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? null;
726
+ toast.error('Falha ao vincular aula.', { description: message ?? undefined });
727
+ } finally {
728
+ setIsLinkingLesson(false);
729
+ }
730
+ };
731
+
732
+ const regenerateWebhookToken = async () => {
733
+ if (!webhookIntegration) return;
734
+ try {
735
+ setIsRegeneratingToken(true);
736
+ const response = await request<BulkUploadRegenerateTokenResponse>({
737
+ url: '/lms/bulk-upload/webhook/regenerate-token',
738
+ method: 'POST',
739
+ });
740
+
741
+ setWebhookPreview(response.data.webhook ?? null);
742
+ setWebhookPlainToken(response.data.webhook?.plainToken ?? null);
743
+
744
+ const lambdaName =
745
+ response.data.infrastructure?.outputs?.lambda_function_name?.trim() ||
746
+ 'Lambda não identificada';
747
+
748
+ const configuredAtRaw =
749
+ response.data.webhook?.updated_at ?? new Date().toISOString();
750
+ const configuredAt = formatDate(configuredAtRaw);
751
+
752
+ setLastSetupFeedback({
753
+ lambdaName,
754
+ configuredAt,
755
+ });
756
+
757
+ toast.success('Token regenerado e infraestrutura sincronizada.', {
758
+ description: `Lambda: ${lambdaName} • Configurado em: ${configuredAt}`,
759
+ });
760
+ await refetchWebhooks();
761
+ } catch {
762
+ toast.error('Não foi possível regenerar o token e sincronizar a Lambda.');
763
+ } finally {
764
+ setIsRegeneratingToken(false);
765
+ }
766
+ };
767
+
768
+ const openCreateProfileSheet = () => {
769
+ setEditingProfileId(null);
770
+ setIsProfileSheetOpen(true);
771
+ };
772
+
773
+ const openEditProfileSheet = () => {
774
+ if (!selectedStorageProfile) {
775
+ return;
776
+ }
777
+
778
+ setEditingProfileId(selectedStorageProfile.id);
779
+ setIsProfileSheetOpen(true);
780
+ };
781
+
782
+ const handleProfileSaved = (profile: IntegrationProfileSheetSavedProfile) => {
783
+ setStorageProfileId(profile.id);
784
+ setProfileRefreshToken((current) => current + 1);
785
+ refetchProfiles();
786
+ };
787
+
788
+ const toggleCleanupStatus = (status: BulkUploadCleanupStatus) => {
789
+ setCleanupStatuses((current) => {
790
+ const next = new Set(current);
791
+ if (next.has(status)) {
792
+ next.delete(status);
793
+ } else {
794
+ next.add(status);
795
+ }
796
+ return next;
797
+ });
798
+ };
799
+
800
+ const cleanupStatusSelection = useMemo(
801
+ () => Array.from(cleanupStatuses),
802
+ [cleanupStatuses]
803
+ );
804
+
805
+ const runCleanupHistory = async () => {
806
+ if (cleanupStatusSelection.length === 0) {
807
+ toast.error('Selecione ao menos um status para limpar.');
808
+ return;
809
+ }
810
+
811
+ try {
812
+ setIsCleaningHistory(true);
813
+ const response = await request<BulkUploadCleanupResponse>({
814
+ url: '/lms/bulk-upload/sessions/cleanup',
815
+ method: 'POST',
816
+ data: {
817
+ statuses: cleanupStatusSelection,
818
+ timeWindow: cleanupTimeWindow,
819
+ },
820
+ });
821
+
822
+ const deletedItems = Number(response.data?.deletedItems ?? 0);
823
+ const deletedSessions = Number(response.data?.deletedSessions ?? 0);
824
+
825
+ toast.success('Historico limpo com sucesso.', {
826
+ description: `${deletedItems} upload(s) e ${deletedSessions} sessao(oes) removidos.`,
827
+ });
828
+
829
+ setIsCleanupDialogOpen(false);
830
+ await refetchSessions();
831
+ } catch {
832
+ toast.error('Nao foi possivel limpar o historico de uploads.');
833
+ } finally {
834
+ setIsCleaningHistory(false);
835
+ }
836
+ };
837
+
838
+ return (
839
+ <Page>
840
+ <PageHeader
841
+ title="Uploads Desktop"
842
+ description="Monitore arquivos enviados via Hedhog Desktop com status, progresso e usuario responsavel."
843
+ breadcrumbs={[
844
+ { label: 'Inicio', href: '/' },
845
+ { label: 'LMS', href: '/lms' },
846
+ { label: 'Uploads Desktop' },
847
+ ]}
848
+ actions={[
849
+ {
850
+ label: 'Limpar historico',
851
+ ariaLabel: 'Limpar historico',
852
+ iconOnly: true,
853
+ variant: 'outline',
854
+ icon: <Trash2 className="h-4 w-4" />,
855
+ onClick: () => setIsCleanupDialogOpen(true),
856
+ },
857
+ {
858
+ label: 'Configurações',
859
+ ariaLabel: 'Configurações',
860
+ iconOnly: true,
861
+ variant: 'outline',
862
+ icon: <Cog className="h-4 w-4" />,
863
+ onClick: () => setSettingsOpen(true),
864
+ },
865
+ ]}
866
+ />
867
+
868
+ <Sheet
869
+ open={settingsOpen}
870
+ onOpenChange={(open) => {
871
+ setSettingsOpen(open);
872
+ if (!open) {
873
+ setWebhookPlainToken(null);
874
+ }
875
+ }}
876
+ >
877
+ <ResizableSheetContent
878
+ sheetId="lms-bulk-upload-settings-sheet"
879
+ defaultWidth={640}
880
+ minWidth={500}
881
+ className="gap-0 overflow-hidden bg-[radial-gradient(circle_at_top,_hsl(var(--primary)/0.09),_transparent_38%),linear-gradient(180deg,_hsl(var(--background)),_hsl(var(--muted)/0.18))]"
882
+ >
883
+ <SheetHeader className="border-b border-border/60 bg-background/85 px-6 pb-5 pt-6 backdrop-blur">
884
+ <div className="flex items-start justify-between gap-4">
885
+ <div className="space-y-2">
886
+ <div className="inline-flex items-center gap-2 rounded-full border border-primary/15 bg-primary/5 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-primary/80">
887
+ <Cog className="h-3.5 w-3.5" />
888
+ Bulk Upload Control
889
+ </div>
890
+ <SheetTitle className="text-lg font-semibold tracking-tight">
891
+ Configurações do upload em massa
892
+ </SheetTitle>
893
+ <SheetDescription className="max-w-xl text-sm text-muted-foreground">
894
+ Configure bucket, credenciais temporárias e webhook para o
895
+ fluxo de upload em massa com provisionamento automático da
896
+ integração.
897
+ </SheetDescription>
898
+ </div>
899
+
900
+ <div className="hidden rounded-2xl border border-border/60 bg-background/80 px-4 py-3 shadow-sm md:block">
901
+ <p className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
902
+ Estado atual
903
+ </p>
904
+ <p className="mt-1 text-sm font-semibold text-foreground">
905
+ {currentStatusLabel}
906
+ </p>
907
+ <p className="mt-1 max-w-[15rem] text-xs text-muted-foreground">
908
+ {currentStatusDescription}
909
+ </p>
910
+ </div>
911
+ </div>
912
+ </SheetHeader>
913
+
914
+ <ScrollArea className="flex-1 overflow-hidden px-6">
915
+ <div className="space-y-6 py-6">
916
+ <section className="overflow-hidden rounded-2xl border border-border/60 bg-background/90 shadow-sm">
917
+ <div className="border-b border-border/50 bg-gradient-to-r from-primary/8 via-primary/4 to-transparent px-5 py-4">
918
+ <h3 className="text-sm font-semibold tracking-tight">
919
+ Instruções rápidas
920
+ </h3>
921
+ <p className="mt-1 text-xs text-muted-foreground">
922
+ A configuração abaixo valida o acesso ao bucket e prepara a
923
+ integração do desktop automaticamente.
924
+ </p>
925
+ </div>
926
+ <ul className="space-y-2 px-5 py-4 text-xs text-muted-foreground">
927
+ <li>
928
+ 1. Informe abaixo a role de execução da Lambda em{' '}
929
+ <span className="font-mono">
930
+ {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
931
+ </span>{' '}
932
+ no grupo LMS.
933
+ </li>
934
+ <li>
935
+ 2. Selecione um perfil de integração do tipo storage (AWS)
936
+ com bucket em{' '}
937
+ <span className="font-mono">config.bucket</span>.
938
+ </li>
939
+ <li>
940
+ 3. Salve a configuração para criar/atualizar Lambda,
941
+ permissões do S3 e webhook automaticamente.
942
+ </li>
943
+ <li>
944
+ 4. Copie URL + token do webhook para o sistema externo.
945
+ </li>
946
+ </ul>
947
+ </section>
948
+
949
+ <section className="space-y-4 rounded-2xl border border-border/60 bg-background/90 p-5 shadow-sm">
950
+ <div className="space-y-1">
951
+ <h3 className="text-sm font-semibold tracking-tight">
952
+ Fonte dos arquivos
953
+ </h3>
954
+ <p className="text-xs text-muted-foreground">
955
+ Escolha o bucket e o perfil S3 que serão usados para
956
+ autenticar uploads e sincronizar a Lambda externa.
957
+ </p>
958
+ </div>
959
+
960
+ <div className="space-y-2">
961
+ <div className="rounded-xl border border-amber-300/40 bg-amber-500/10 p-3">
962
+ <p className="text-xs font-medium text-amber-800 dark:text-amber-300">
963
+ Role de execução da Lambda
964
+ </p>
965
+ <p className="mt-1 text-xs text-amber-800/90 dark:text-amber-300/90">
966
+ Essa role será usada para criar a Lambda automaticamente
967
+ quando necessário. Informe o número da conta AWS e o nome
968
+ da role IAM de execução da Lambda antes de salvar a
969
+ configuração.
970
+ </p>
971
+ </div>
972
+
973
+ <div className="space-y-2">
974
+ <div className="flex gap-2">
975
+ <div className="flex-none space-y-1">
976
+ <Label htmlFor="bulk-upload-aws-account-id">
977
+ Conta AWS
978
+ </Label>
979
+ <Input
980
+ id="bulk-upload-aws-account-id"
981
+ placeholder="123456789012"
982
+ maxLength={12}
983
+ value={awsAccountId}
984
+ onChange={(event) =>
985
+ setAwsAccountId(event.target.value.replace(/\D/g, '').slice(0, 12))
986
+ }
987
+ disabled={settingsLoading || isSavingSettings}
988
+ className={
989
+ awsAccountId && !isAccountIdValid
990
+ ? 'border-destructive focus-visible:ring-destructive'
991
+ : ''
992
+ }
993
+ />
994
+ {awsAccountId && !isAccountIdValid && (
995
+ <p className="text-xs text-destructive">
996
+ Deve ter 12 dígitos.
997
+ </p>
998
+ )}
999
+ </div>
1000
+ <div className="flex-1 space-y-1">
1001
+ <Label htmlFor="bulk-upload-lambda-role-name">
1002
+ Nome da role IAM
1003
+ </Label>
1004
+ <Input
1005
+ id="bulk-upload-lambda-role-name"
1006
+ placeholder="lms-bulk-upload-lambda-role"
1007
+ value={lambdaRoleName}
1008
+ onChange={(event) => setLambdaRoleName(event.target.value)}
1009
+ disabled={settingsLoading || isSavingSettings}
1010
+ className={
1011
+ lambdaRoleName && !isRoleNameValid
1012
+ ? 'border-destructive focus-visible:ring-destructive'
1013
+ : ''
1014
+ }
1015
+ />
1016
+ {lambdaRoleName && !isRoleNameValid && (
1017
+ <p className="text-xs text-destructive">
1018
+ Nome de role inválido.
1019
+ </p>
1020
+ )}
1021
+ </div>
1022
+ </div>
1023
+ {lambdaRoleArn ? (
1024
+ <p className="font-mono text-xs text-muted-foreground">
1025
+ {lambdaRoleArn}
1026
+ </p>
1027
+ ) : (
1028
+ <p className="text-xs text-muted-foreground">
1029
+ Setting: {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
1030
+ </p>
1031
+ )}
1032
+ </div>
1033
+
1034
+ <Label>Perfil de integração de storage</Label>
1035
+ <div className="rounded-2xl border border-border/60 bg-muted/20 p-3">
1036
+ <div className="flex items-center gap-2">
1037
+ <EntityPicker<IntegrationProfileOption>
1038
+ className="flex-1"
1039
+ buttonClassName="border-border/60 bg-background shadow-sm"
1040
+ placeholder="Selecione um perfil de integração"
1041
+ options={storageProfileOptions}
1042
+ value={storageProfileId}
1043
+ valueType="number"
1044
+ getOptionValue={(option) => option.id}
1045
+ getOptionLabel={(option) => option.name}
1046
+ getOptionDescription={(option) => option.slug}
1047
+ onChange={(value) =>
1048
+ setStorageProfileId(
1049
+ value === null ? null : Number(value)
1050
+ )
1051
+ }
1052
+ clearable
1053
+ searchable
1054
+ showCreateButton={false}
1055
+ loadingLabel={
1056
+ profilesLoading ? 'Carregando...' : undefined
1057
+ }
1058
+ />
1059
+
1060
+ <TooltipProvider>
1061
+ <Tooltip>
1062
+ <TooltipTrigger asChild>
1063
+ <Button
1064
+ type="button"
1065
+ variant="outline"
1066
+ size="icon"
1067
+ className="shrink-0 border-border/60 bg-background shadow-sm"
1068
+ onClick={openCreateProfileSheet}
1069
+ aria-label="Criar perfil"
1070
+ >
1071
+ <Plus className="h-4 w-4" />
1072
+ </Button>
1073
+ </TooltipTrigger>
1074
+ <TooltipContent>Criar perfil</TooltipContent>
1075
+ </Tooltip>
1076
+
1077
+ {selectedStorageProfile ? (
1078
+ <Tooltip>
1079
+ <TooltipTrigger asChild>
1080
+ <Button
1081
+ type="button"
1082
+ variant="outline"
1083
+ size="icon"
1084
+ className="shrink-0 border-border/60 bg-background shadow-sm"
1085
+ onClick={openEditProfileSheet}
1086
+ aria-label="Editar perfil"
1087
+ >
1088
+ <Pencil className="h-4 w-4" />
1089
+ </Button>
1090
+ </TooltipTrigger>
1091
+ <TooltipContent>Editar perfil</TooltipContent>
1092
+ </Tooltip>
1093
+ ) : null}
1094
+ </TooltipProvider>
1095
+ </div>
1096
+
1097
+ <p className="mt-3 text-xs text-muted-foreground">
1098
+ O seletor já permite limpar a seleção atual. Use os
1099
+ atalhos laterais apenas para criar ou editar o perfil
1100
+ ativo.
1101
+ </p>
1102
+ </div>
1103
+
1104
+ <p className="text-xs text-muted-foreground">
1105
+ Setting: {BULK_UPLOAD_STORAGE_PROFILE_SLUG}
1106
+ </p>
1107
+ </div>
1108
+
1109
+ <div className="space-y-2">
1110
+ <Label>Bucket S3 (do perfil selecionado)</Label>
1111
+ <Input
1112
+ readOnly
1113
+ value={
1114
+ bucketName ||
1115
+ 'Selecione e salve um perfil para carregar o bucket'
1116
+ }
1117
+ disabled={settingsLoading}
1118
+ />
1119
+ <p className="text-xs text-muted-foreground">
1120
+ O bucket vem do campo{' '}
1121
+ <span className="font-mono">config.bucket</span> do perfil
1122
+ de integração.
1123
+ </p>
1124
+ </div>
1125
+
1126
+ <div className="rounded-2xl border border-dashed border-border/70 bg-muted/15 p-4">
1127
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1128
+ <div className="space-y-1">
1129
+ <p className="text-sm font-medium">Sincronização final</p>
1130
+ <p className="text-xs text-muted-foreground">
1131
+ O salvamento valida o bucket, testa as credenciais
1132
+ temporárias e atualiza o webhook usado pela Lambda.
1133
+ </p>
1134
+ </div>
1135
+
1136
+ {storageProfileId && lambdaRoleArn.trim() ? (
1137
+ <Button
1138
+ type="button"
1139
+ onClick={saveBulkUploadSettings}
1140
+ disabled={isSavingSettings}
1141
+ className="gap-2 shadow-sm"
1142
+ >
1143
+ {isSavingSettings ? (
1144
+ <Loader2 className="h-4 w-4 animate-spin" />
1145
+ ) : (
1146
+ <Save className="h-4 w-4" />
1147
+ )}
1148
+ Salvar configuração
1149
+ </Button>
1150
+ ) : (
1151
+ <p className="text-xs text-muted-foreground">
1152
+ Informe a role ARN e selecione um perfil para habilitar
1153
+ o salvamento.
1154
+ </p>
1155
+ )}
1156
+ </div>
1157
+
1158
+ {lastSetupFeedback ? (
1159
+ <div className="mt-3 rounded-xl border border-emerald-400/30 bg-emerald-500/10 p-3">
1160
+ <p className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
1161
+ Tudo pronto
1162
+ </p>
1163
+ <p className="mt-1 text-xs text-emerald-700/90 dark:text-emerald-300/90">
1164
+ Função Lambda: {lastSetupFeedback.lambdaName}
1165
+ </p>
1166
+ <p className="text-xs text-emerald-700/90 dark:text-emerald-300/90">
1167
+ Configurado em: {lastSetupFeedback.configuredAt}
1168
+ </p>
1169
+ </div>
1170
+ ) : null}
1171
+ </div>
1172
+ </section>
1173
+
1174
+ <section className="space-y-4">
1175
+ <div className="flex items-center justify-between gap-2">
1176
+ <h3 className="flex items-center gap-2 text-sm font-semibold">
1177
+ <Webhook className="h-4 w-4" />
1178
+ Webhook de integração
1179
+ </h3>
1180
+
1181
+ {webhookIntegration ? (
1182
+ <Badge
1183
+ variant={
1184
+ webhookIntegration.status === 'active'
1185
+ ? 'default'
1186
+ : 'outline'
1187
+ }
1188
+ >
1189
+ {webhookIntegration.status === 'active'
1190
+ ? 'Ativo'
1191
+ : 'Inativo'}
1192
+ </Badge>
1193
+ ) : null}
1194
+ </div>
1195
+
1196
+ {!webhookIntegration ? (
1197
+ <div className="space-y-3 rounded-lg border bg-muted/20 p-4">
1198
+ <p className="text-sm text-muted-foreground">
1199
+ O webhook será criado automaticamente após salvar uma
1200
+ configuração válida (bucket + perfil), com URL e segredo
1201
+ exibidos abaixo.
1202
+ </p>
1203
+ </div>
1204
+ ) : (
1205
+ <div className="space-y-3 rounded-lg border bg-muted/20 p-4">
1206
+ <div className="space-y-1">
1207
+ <Label>URL do webhook</Label>
1208
+ <div className="flex items-center gap-2">
1209
+ <Input
1210
+ readOnly
1211
+ value={webhookIntegration.public_url ?? ''}
1212
+ />
1213
+ <CopyButton
1214
+ value={webhookIntegration.public_url ?? ''}
1215
+ />
1216
+ <TooltipProvider>
1217
+ <Tooltip>
1218
+ <TooltipTrigger asChild>
1219
+ <Button
1220
+ type="button"
1221
+ variant="outline"
1222
+ size="icon"
1223
+ onClick={() =>
1224
+ window.open(
1225
+ '/core/integrations-webhooks',
1226
+ '_blank'
1227
+ )
1228
+ }
1229
+ aria-label="Abrir webhooks"
1230
+ >
1231
+ <ExternalLink className="h-4 w-4" />
1232
+ </Button>
1233
+ </TooltipTrigger>
1234
+ <TooltipContent>
1235
+ Abrir tela de webhooks
1236
+ </TooltipContent>
1237
+ </Tooltip>
1238
+ </TooltipProvider>
1239
+ </div>
1240
+ </div>
1241
+
1242
+ <div className="space-y-1">
1243
+ <Label>Token do webhook</Label>
1244
+ <div className="flex items-center gap-2">
1245
+ <Input
1246
+ readOnly
1247
+ value={
1248
+ webhookPlainToken ??
1249
+ 'Token oculto. Regenerar para visualizar novo token.'
1250
+ }
1251
+ />
1252
+ {webhookPlainToken ? (
1253
+ <CopyButton value={webhookPlainToken} />
1254
+ ) : null}
1255
+ <Button
1256
+ type="button"
1257
+ variant="outline"
1258
+ className="gap-2"
1259
+ onClick={regenerateWebhookToken}
1260
+ disabled={isRegeneratingToken}
1261
+ >
1262
+ {isRegeneratingToken ? (
1263
+ <Loader2 className="h-4 w-4 animate-spin" />
1264
+ ) : (
1265
+ <RefreshCw className="h-4 w-4" />
1266
+ )}
1267
+ Regenerar token
1268
+ </Button>
1269
+ </div>
1270
+ </div>
1271
+ </div>
1272
+ )}
1273
+ </section>
1274
+ </div>
1275
+ </ScrollArea>
1276
+ </ResizableSheetContent>
1277
+ </Sheet>
1278
+
1279
+ <AlertDialog
1280
+ open={isCleanupDialogOpen}
1281
+ onOpenChange={(open) => {
1282
+ if (!isCleaningHistory) {
1283
+ setIsCleanupDialogOpen(open);
1284
+ }
1285
+ }}
1286
+ >
1287
+ <AlertDialogContent>
1288
+ <AlertDialogHeader>
1289
+ <AlertDialogTitle>Limpar historico de uploads</AlertDialogTitle>
1290
+ <AlertDialogDescription>
1291
+ Selecione quais status e intervalo de tempo devem ser removidos.
1292
+ Esta acao remove apenas registros do banco e nao afeta arquivos no
1293
+ S3.
1294
+ </AlertDialogDescription>
1295
+ </AlertDialogHeader>
1296
+
1297
+ <div className="space-y-4 py-2">
1298
+ <div className="space-y-2">
1299
+ <Label>Status para limpar</Label>
1300
+ <div className="space-y-2 rounded-lg border border-border/70 bg-muted/20 p-3">
1301
+ {CLEANUP_STATUS_OPTIONS.map((option) => (
1302
+ <div key={option.value} className="flex items-center gap-2">
1303
+ <Checkbox
1304
+ id={`cleanup-status-${option.value}`}
1305
+ checked={cleanupStatuses.has(option.value)}
1306
+ onCheckedChange={() => toggleCleanupStatus(option.value)}
1307
+ disabled={isCleaningHistory}
1308
+ />
1309
+ <Label
1310
+ htmlFor={`cleanup-status-${option.value}`}
1311
+ className="cursor-pointer text-sm font-normal"
1312
+ >
1313
+ {option.label}
1314
+ </Label>
1315
+ </div>
1316
+ ))}
1317
+ </div>
1318
+ </div>
1319
+
1320
+ <div className="space-y-2">
1321
+ <Label>Periodo da limpeza</Label>
1322
+ <div className="grid gap-2 sm:grid-cols-2">
1323
+ {CLEANUP_TIME_WINDOW_OPTIONS.map((option) => (
1324
+ <button
1325
+ key={option.value}
1326
+ type="button"
1327
+ onClick={() => setCleanupTimeWindow(option.value)}
1328
+ className={cn(
1329
+ 'rounded-lg border p-3 text-left transition-colors',
1330
+ cleanupTimeWindow === option.value
1331
+ ? 'border-primary bg-primary/10'
1332
+ : 'border-border/70 bg-background hover:border-primary/40'
1333
+ )}
1334
+ disabled={isCleaningHistory}
1335
+ >
1336
+ <p className="text-sm font-medium">{option.label}</p>
1337
+ <p className="mt-1 text-xs text-muted-foreground">
1338
+ {option.description}
1339
+ </p>
1340
+ </button>
1341
+ ))}
1342
+ </div>
1343
+ </div>
1344
+
1345
+ <div className="rounded-lg border border-amber-300/50 bg-amber-500/10 p-3 text-xs text-amber-900 dark:text-amber-200">
1346
+ Esta operacao e irreversivel e pode remover muitos registros de
1347
+ uma vez.
1348
+ </div>
1349
+ </div>
1350
+
1351
+ <div className="flex justify-end gap-2">
1352
+ <AlertDialogCancel disabled={isCleaningHistory}>
1353
+ Cancelar
1354
+ </AlertDialogCancel>
1355
+ <AlertDialogAction
1356
+ className="bg-red-600 text-white hover:bg-red-700"
1357
+ disabled={
1358
+ isCleaningHistory || cleanupStatusSelection.length === 0
1359
+ }
1360
+ onClick={(event) => {
1361
+ event.preventDefault();
1362
+ runCleanupHistory();
1363
+ }}
1364
+ >
1365
+ {isCleaningHistory ? (
1366
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1367
+ ) : (
1368
+ <Trash2 className="mr-2 h-4 w-4" />
1369
+ )}
1370
+ Limpar historico
1371
+ </AlertDialogAction>
1372
+ </div>
1373
+ </AlertDialogContent>
1374
+ </AlertDialog>
1375
+
1376
+ <IntegrationProfileSheet
1377
+ open={isProfileSheetOpen}
1378
+ onOpenChange={setIsProfileSheetOpen}
1379
+ profileId={editingProfileId}
1380
+ lockedTypeSlug="storage"
1381
+ lockedProviderSlug="s3"
1382
+ sheetId="lms-bulk-upload-storage-profile-editor"
1383
+ defaultWidth={760}
1384
+ onSaved={handleProfileSaved}
1385
+ />
1386
+
1387
+ <KpiCardsGrid items={kpiItems} columns={3} />
1388
+
1389
+ <div className="flex items-center justify-between gap-3">
1390
+ <SearchBar
1391
+ searchQuery={searchInput}
1392
+ onSearchChange={setSearchInput}
1393
+ onSearch={() => {
1394
+ setSearch(searchInput);
1395
+ setPage(1);
1396
+ }}
1397
+ placeholder="Buscar por arquivo, app ou usuario"
1398
+ controls={[
1399
+ {
1400
+ id: 'status-filter',
1401
+ type: 'select',
1402
+ value: statusFilter,
1403
+ onChange: (value) => {
1404
+ setStatusFilter(value);
1405
+ setPage(1);
1406
+ },
1407
+ options: [
1408
+ { value: 'all', label: 'Todos os status' },
1409
+ { value: 'queued', label: 'Na fila' },
1410
+ { value: 'uploading', label: 'Enviando' },
1411
+ { value: 'cancelling', label: 'Cancelando' },
1412
+ { value: 'done', label: 'Aguardando confirmação' },
1413
+ { value: 'received', label: 'Concluido' },
1414
+ { value: 'cancelled', label: 'Cancelado' },
1415
+ { value: 'error', label: 'Erro' },
1416
+ ],
1417
+ },
1418
+ ]}
1419
+ />
1420
+ </div>
1421
+
1422
+ <div className="overflow-hidden rounded-xl border border-border bg-card">
1423
+ <Table>
1424
+ <TableHeader>
1425
+ <TableRow>
1426
+ <TableHead>Arquivo</TableHead>
1427
+ <TableHead>Tamanho</TableHead>
1428
+ <TableHead>Usuario</TableHead>
1429
+ <TableHead>Aula vinculada</TableHead>
1430
+ <TableHead>Status</TableHead>
1431
+ <TableHead className="w-40">Progresso</TableHead>
1432
+ <TableHead>Atualizado em</TableHead>
1433
+ </TableRow>
1434
+ </TableHeader>
1435
+ <TableBody>
1436
+ {isLoading ? (
1437
+ Array.from({ length: 8 }).map((_, index) => (
1438
+ <TableRow key={`skeleton-${index}`}>
1439
+ <TableCell colSpan={7}>
1440
+ <Skeleton className="h-6 w-full" />
1441
+ </TableCell>
1442
+ </TableRow>
1443
+ ))
1444
+ ) : rows.length === 0 ? (
1445
+ <TableRow>
1446
+ <TableCell colSpan={7} className="py-8">
1447
+ <EmptyState
1448
+ icon={<HardDriveUpload className="h-5 w-5" />}
1449
+ title="Nenhum upload encontrado"
1450
+ description="Inicie uploads no Hedhog Desktop para visualizar o historico e andamento aqui."
1451
+ />
1452
+ </TableCell>
1453
+ </TableRow>
1454
+ ) : (
1455
+ rows.map((row) => {
1456
+ const meta = getStatusMeta(row.status);
1457
+ const StatusIcon = meta.icon;
1458
+ const pct = Math.max(0, Math.min(100, row.progressPercent));
1459
+
1460
+ return (
1461
+ <TableRow key={row.id} className="cursor-pointer">
1462
+ <TableCell className="max-w-80">
1463
+ <div className="flex items-start gap-2.5">
1464
+ <span className="mt-0.5 shrink-0">
1465
+ <FileTypeIcon filename={row.fileName} size={20} />
1466
+ </span>
1467
+ <div className="min-w-0 space-y-0.5">
1468
+ <p className="truncate text-sm font-medium">
1469
+ {row.fileName}
1470
+ </p>
1471
+ <p className="truncate text-xs text-muted-foreground">
1472
+ Sessao #{row.sessionId} &middot; {row.appName}
1473
+ </p>
1474
+ {row.errorMessage ? (
1475
+ <p className="truncate text-xs text-destructive">
1476
+ {row.errorMessage}
1477
+ </p>
1478
+ ) : null}
1479
+ </div>
1480
+ </div>
1481
+ </TableCell>
1482
+
1483
+ <TableCell className="tabular-nums text-sm">
1484
+ {formatBytes(row.sizeBytes)}
1485
+ </TableCell>
1486
+
1487
+ <TableCell>
1488
+ <div className="flex items-center gap-2">
1489
+ <Avatar className="h-7 w-7 shrink-0">
1490
+ <AvatarImage
1491
+ src={getPhotoUrl(row.userPhotoId)}
1492
+ alt={row.userName ?? ''}
1493
+ />
1494
+ <AvatarFallback className="bg-muted text-[10px] font-medium">
1495
+ {row.userName
1496
+ ? row.userName
1497
+ .split(' ')
1498
+ .slice(0, 2)
1499
+ .map((part) => part[0])
1500
+ .join('')
1501
+ .toUpperCase()
1502
+ : 'U'}
1503
+ </AvatarFallback>
1504
+ </Avatar>
1505
+ <div className="min-w-0">
1506
+ <p className="truncate text-sm">
1507
+ {row.userName || 'Usuario nao identificado'}
1508
+ </p>
1509
+ </div>
1510
+ </div>
1511
+ </TableCell>
1512
+
1513
+ <TableCell className="max-w-[22rem]">
1514
+ {row.matchedCourseId && row.matchedLessonId ? (
1515
+ <button
1516
+ type="button"
1517
+ className="flex w-full items-center gap-2 text-left"
1518
+ onClick={() =>
1519
+ window.open(
1520
+ `/lms/courses/${row.matchedCourseId}`,
1521
+ '_blank',
1522
+ 'noopener,noreferrer'
1523
+ )
1524
+ }
1525
+ >
1526
+ <Avatar className="h-7 w-7 shrink-0 rounded-md">
1527
+ <AvatarImage
1528
+ src={getPhotoUrl(row.matchedCourseLogoFileId)}
1529
+ alt={row.matchedCourseTitle ?? ''}
1530
+ />
1531
+ <AvatarFallback className="rounded-md bg-muted text-[10px] font-medium">
1532
+ {(row.matchedCourseTitle ?? 'Curso')
1533
+ .split(' ')
1534
+ .slice(0, 2)
1535
+ .map((part) => part[0])
1536
+ .join('')
1537
+ .toUpperCase()}
1538
+ </AvatarFallback>
1539
+ </Avatar>
1540
+ <div className="min-w-0 space-y-0.5">
1541
+ <p className="truncate text-sm font-medium text-primary underline-offset-4 hover:underline">
1542
+ {row.matchedLessonTitle ??
1543
+ `Aula #${row.matchedLessonId}`}
1544
+ </p>
1545
+ <p className="truncate text-xs text-muted-foreground">
1546
+ {row.matchedSessionTitle
1547
+ ? `Sessão ${row.matchedSessionTitle}`
1548
+ : `Sessão #${row.matchedSessionId ?? '-'}`}{' '}
1549
+ ·{' '}
1550
+ {row.matchedCourseTitle ??
1551
+ row.matchedCourseSlug ??
1552
+ 'Curso não identificado'}
1553
+ </p>
1554
+ </div>
1555
+ </button>
1556
+ ) : row.status === 'lesson_not_found' ? (
1557
+ <Button
1558
+ variant="outline"
1559
+ size="sm"
1560
+ className="gap-1.5 text-xs"
1561
+ onClick={() => openManualLink(row.id)}
1562
+ >
1563
+ <Link2 className="h-3.5 w-3.5" />
1564
+ Vincular
1565
+ </Button>
1566
+ ) : (
1567
+ <span className="text-sm text-muted-foreground">-</span>
1568
+ )}
1569
+ </TableCell>
1570
+
1571
+ <TableCell>
1572
+ <div className="flex items-center gap-1.5">
1573
+ <StatusIcon
1574
+ className={cn('h-3.5 w-3.5 shrink-0', meta.iconClass)}
1575
+ />
1576
+ <Badge variant={meta.variant}>{meta.label}</Badge>
1577
+ </div>
1578
+ </TableCell>
1579
+
1580
+ <TableCell>
1581
+ <div className="space-y-1">
1582
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
1583
+ <span
1584
+ className={cn(
1585
+ 'tabular-nums font-medium',
1586
+ meta.animated && 'text-foreground'
1587
+ )}
1588
+ >
1589
+ {pct}%
1590
+ </span>
1591
+ </div>
1592
+ <Progress
1593
+ value={pct}
1594
+ className={cn(
1595
+ 'h-1.5',
1596
+ meta.animated &&
1597
+ '[&>[data-slot=progress-indicator]]:animate-pulse'
1598
+ )}
1599
+ />
1600
+ </div>
1601
+ </TableCell>
1602
+
1603
+ <TableCell>
1604
+ <div className="space-y-0.5">
1605
+ <p className="text-sm">{formatDate(row.updatedAt)}</p>
1606
+ <p className="text-xs text-muted-foreground">
1607
+ Finalizado: {formatDate(row.completedAt)}
1608
+ </p>
1609
+ </div>
1610
+ </TableCell>
1611
+ </TableRow>
1612
+ );
1613
+ })
1614
+ )}
1615
+ </TableBody>
1616
+ </Table>
1617
+ </div>
1618
+
1619
+ <PaginationFooter
1620
+ currentPage={page}
1621
+ pageSize={pageSize}
1622
+ totalItems={total}
1623
+ onPageChange={setPage}
1624
+ onPageSizeChange={(nextPageSize) => {
1625
+ setPageSize(nextPageSize);
1626
+ setPage(1);
1627
+ }}
1628
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
1629
+ />
1630
+
1631
+ <Sheet open={manualLinkOpen} onOpenChange={setManualLinkOpen}>
1632
+ <ResizableSheetContent
1633
+ sheetId="lms-bulk-upload-manual-link-sheet"
1634
+ defaultWidth={560}
1635
+ minWidth={400}
1636
+ className="flex flex-col gap-0 overflow-hidden"
1637
+ >
1638
+ <SheetHeader className="border-b px-6 py-4">
1639
+ <SheetTitle className="flex items-center gap-2">
1640
+ <Link2 className="h-4 w-4" />
1641
+ Vincular aula manualmente
1642
+ </SheetTitle>
1643
+ <SheetDescription>
1644
+ Selecione o curso, módulo e aula para associar este vídeo e iniciar o processamento.
1645
+ </SheetDescription>
1646
+ </SheetHeader>
1647
+
1648
+ <ScrollArea className="flex-1">
1649
+ <div className="space-y-6 p-6">
1650
+ <div className="overflow-hidden rounded-xl border border-border/60 bg-black">
1651
+ {manualLinkVideoLoading ? (
1652
+ <div className="flex h-40 items-center justify-center">
1653
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
1654
+ </div>
1655
+ ) : manualLinkVideoUrl ? (
1656
+ <video
1657
+ src={manualLinkVideoUrl}
1658
+ controls
1659
+ className="w-full"
1660
+ style={{ maxHeight: 280 }}
1661
+ />
1662
+ ) : (
1663
+ <div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
1664
+ Pré-visualização indisponível
1665
+ </div>
1666
+ )}
1667
+ </div>
1668
+
1669
+ <div className="space-y-4">
1670
+ <div className="space-y-2">
1671
+ <Label>Curso</Label>
1672
+ <EntityPicker<{ id: number; title: string; code: string | null }>
1673
+ placeholder="Selecione um curso"
1674
+ options={coursesResult?.data ?? []}
1675
+ value={manualLinkCourseId}
1676
+ valueType="number"
1677
+ getOptionValue={(o) => o.id}
1678
+ getOptionLabel={(o) => o.title}
1679
+ getOptionDescription={(o) => o.code ?? ''}
1680
+ onChange={(value) => {
1681
+ setManualLinkCourseId(value as number | null);
1682
+ if (value) loadCourseStructure(value as number);
1683
+ }}
1684
+ />
1685
+ </div>
1686
+
1687
+ <div className="space-y-2">
1688
+ <Label>Módulo / Sessão</Label>
1689
+ <EntityPicker<{ id: number; title: string }>
1690
+ placeholder={
1691
+ !manualLinkCourseId
1692
+ ? 'Selecione um curso primeiro'
1693
+ : manualLinkStructureLoading
1694
+ ? 'Carregando...'
1695
+ : 'Selecione um módulo'
1696
+ }
1697
+ disabled={!manualLinkCourseId || manualLinkStructureLoading}
1698
+ options={manualLinkStructure?.sessions ?? []}
1699
+ value={manualLinkSessionId}
1700
+ valueType="number"
1701
+ getOptionValue={(o) => o.id}
1702
+ getOptionLabel={(o) => o.title}
1703
+ onChange={(value) => {
1704
+ setManualLinkSessionId(value as number | null);
1705
+ setManualLinkLessonId(null);
1706
+ }}
1707
+ />
1708
+ </div>
1709
+
1710
+ <div className="space-y-2">
1711
+ <Label>Aula</Label>
1712
+ <EntityPicker<{ id: number; title: string }>
1713
+ placeholder={
1714
+ !manualLinkSessionId ? 'Selecione um módulo primeiro' : 'Selecione uma aula'
1715
+ }
1716
+ disabled={!manualLinkSessionId}
1717
+ options={
1718
+ manualLinkStructure?.sessions.find((s) => s.id === manualLinkSessionId)?.lessons ?? []
1719
+ }
1720
+ value={manualLinkLessonId}
1721
+ valueType="number"
1722
+ getOptionValue={(o) => o.id}
1723
+ getOptionLabel={(o) => o.title}
1724
+ onChange={(value) => setManualLinkLessonId(value as number | null)}
1725
+ />
1726
+ </div>
1727
+ </div>
1728
+ </div>
1729
+ </ScrollArea>
1730
+
1731
+ <div className="border-t px-6 py-4">
1732
+ <Button
1733
+ className="w-full gap-2"
1734
+ disabled={!manualLinkCourseId || !manualLinkSessionId || !manualLinkLessonId || isLinkingLesson}
1735
+ onClick={confirmManualLink}
1736
+ >
1737
+ {isLinkingLesson ? (
1738
+ <Loader2 className="h-4 w-4 animate-spin" />
1739
+ ) : (
1740
+ <Link2 className="h-4 w-4" />
1741
+ )}
1742
+ Confirmar vínculo e iniciar processamento
1743
+ </Button>
1744
+ </div>
1745
+ </ResizableSheetContent>
1746
+ </Sheet>
1747
+ </Page>
1748
+ );
1749
+ }