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