@hed-hog/lms 0.0.364 → 0.0.366

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 (290) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/class-group/class-group.controller.d.ts +1 -0
  6. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  7. package/dist/class-group/class-group.service.d.ts +1 -0
  8. package/dist/class-group/class-group.service.d.ts.map +1 -1
  9. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  10. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  12. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  13. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  14. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  15. package/dist/course/course-export-scorm12.service.js +628 -0
  16. package/dist/course/course-export-scorm12.service.js.map +1 -0
  17. package/dist/course/course-export.service.d.ts +84 -0
  18. package/dist/course/course-export.service.d.ts.map +1 -0
  19. package/dist/course/course-export.service.js +237 -0
  20. package/dist/course/course-export.service.js.map +1 -0
  21. package/dist/course/course-structure.controller.d.ts +20 -10
  22. package/dist/course/course-structure.controller.d.ts.map +1 -1
  23. package/dist/course/course-structure.controller.js +20 -4
  24. package/dist/course/course-structure.controller.js.map +1 -1
  25. package/dist/course/course-structure.service.d.ts +12 -4
  26. package/dist/course/course-structure.service.d.ts.map +1 -1
  27. package/dist/course/course-structure.service.js +98 -23
  28. package/dist/course/course-structure.service.js.map +1 -1
  29. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  30. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  31. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  32. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  33. package/dist/course/course-video-hls.service.d.ts +71 -0
  34. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  35. package/dist/course/course-video-hls.service.js +784 -0
  36. package/dist/course/course-video-hls.service.js.map +1 -0
  37. package/dist/course/course.controller.d.ts +47 -13
  38. package/dist/course/course.controller.d.ts.map +1 -1
  39. package/dist/course/course.controller.js +40 -26
  40. package/dist/course/course.controller.js.map +1 -1
  41. package/dist/course/course.mcp-tools.js +1 -1
  42. package/dist/course/course.mcp-tools.js.map +1 -1
  43. package/dist/course/course.module.d.ts.map +1 -1
  44. package/dist/course/course.module.js +16 -0
  45. package/dist/course/course.module.js.map +1 -1
  46. package/dist/course/course.service.d.ts +8 -9
  47. package/dist/course/course.service.d.ts.map +1 -1
  48. package/dist/course/course.service.js +93 -50
  49. package/dist/course/course.service.js.map +1 -1
  50. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  51. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  52. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  53. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  54. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  55. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  56. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  57. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  58. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  59. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  62. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  63. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  64. package/dist/course/dto/create-course-export.dto.js +71 -0
  65. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  66. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  67. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  68. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  69. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  70. package/dist/course/ffmpeg.util.d.ts +10 -0
  71. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  72. package/dist/course/ffmpeg.util.js +79 -0
  73. package/dist/course/ffmpeg.util.js.map +1 -0
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts +18 -1
  75. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload-automation.service.js +106 -8
  77. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  78. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  79. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  80. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  81. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  82. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  83. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  84. package/dist/course/lms-bulk-upload.controller.js +43 -2
  85. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  86. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  87. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  88. package/dist/course/lms-bulk-upload.service.js +59 -6
  89. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  90. package/dist/course/lms-setting.controller.d.ts +2 -1
  91. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  92. package/dist/course/lms-setting.controller.js +4 -2
  93. package/dist/course/lms-setting.controller.js.map +1 -1
  94. package/dist/course/scorm12-schemas.d.ts +4 -0
  95. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  96. package/dist/course/scorm12-schemas.js +9 -0
  97. package/dist/course/scorm12-schemas.js.map +1 -0
  98. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  99. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  100. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  101. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  102. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  103. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  104. package/dist/enterprise/training/training-student.service.js +217 -4
  105. package/dist/enterprise/training/training-student.service.js.map +1 -1
  106. package/dist/evaluation/evaluation.service.d.ts +18 -0
  107. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  108. package/dist/evaluation/evaluation.service.js +125 -0
  109. package/dist/evaluation/evaluation.service.js.map +1 -1
  110. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  111. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  112. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  113. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  114. package/dist/exam/exam.module.d.ts.map +1 -1
  115. package/dist/exam/exam.module.js +2 -1
  116. package/dist/exam/exam.module.js.map +1 -1
  117. package/dist/exam/exam.service.d.ts +21 -0
  118. package/dist/exam/exam.service.d.ts.map +1 -1
  119. package/dist/exam/exam.service.js +80 -0
  120. package/dist/exam/exam.service.js.map +1 -1
  121. package/dist/exam/question.controller.d.ts +27 -0
  122. package/dist/exam/question.controller.d.ts.map +1 -0
  123. package/dist/exam/question.controller.js +53 -0
  124. package/dist/exam/question.controller.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  126. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  127. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  128. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  129. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  130. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  131. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  132. package/dist/lms-commerce-access.subscriber.js +74 -0
  133. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +16 -5
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  138. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  140. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  141. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  142. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  143. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  144. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  145. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  146. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  147. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  148. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  149. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  150. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  151. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  152. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  153. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  154. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  155. package/dist/platforma/platforma-performance.service.js +500 -0
  156. package/dist/platforma/platforma-performance.service.js.map +1 -0
  157. package/dist/platforma/platforma-search.service.d.ts +21 -0
  158. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  159. package/dist/platforma/platforma-search.service.js +64 -0
  160. package/dist/platforma/platforma-search.service.js.map +1 -0
  161. package/dist/platforma/platforma-video.service.d.ts +39 -0
  162. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  163. package/dist/platforma/platforma-video.service.js +301 -0
  164. package/dist/platforma/platforma-video.service.js.map +1 -0
  165. package/dist/platforma/platforma.controller.d.ts +209 -1
  166. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  167. package/dist/platforma/platforma.controller.js +208 -2
  168. package/dist/platforma/platforma.controller.js.map +1 -1
  169. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  170. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  171. package/dist/realtime/lms-realtime.controller.js +31 -0
  172. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  173. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  174. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  175. package/dist/realtime/lms-realtime.service.js.map +1 -1
  176. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  177. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  178. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  179. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  180. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  181. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  182. package/dist/student-xp/student-xp.controller.js +24 -0
  183. package/dist/student-xp/student-xp.controller.js.map +1 -1
  184. package/dist/student-xp/student-xp.service.d.ts +16 -0
  185. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  186. package/dist/student-xp/student-xp.service.js +51 -1
  187. package/dist/student-xp/student-xp.service.js.map +1 -1
  188. package/hedhog/data/evaluation_topic.yaml +17 -0
  189. package/hedhog/data/menu.yaml +0 -17
  190. package/hedhog/data/queue_definition.yaml +48 -0
  191. package/hedhog/data/route.yaml +94 -124
  192. package/hedhog/data/setting_group.yaml +19 -19
  193. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  194. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  195. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  196. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  197. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  198. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  199. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  200. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +158 -45
  201. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  202. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  203. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  204. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  205. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  206. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  207. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  208. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  209. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  210. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  211. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  212. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  213. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  214. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  215. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  216. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -1
  217. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  218. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  219. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  220. package/hedhog/frontend/app/courses/page.tsx.ejs +85 -9
  221. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  222. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  223. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  224. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  225. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  226. package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
  227. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  228. package/hedhog/frontend/messages/en.json +44 -28
  229. package/hedhog/frontend/messages/pt.json +47 -29
  230. package/hedhog/table/course_enrollment.yaml +3 -0
  231. package/hedhog/table/course_export.yaml +62 -0
  232. package/hedhog/table/lesson_view_event.yaml +66 -0
  233. package/package.json +14 -9
  234. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  235. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  236. package/src/course/course-export-scorm12.service.ts +668 -0
  237. package/src/course/course-export.service.ts +280 -0
  238. package/src/course/course-structure.controller.ts +16 -2
  239. package/src/course/course-structure.service.ts +100 -7
  240. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  241. package/src/course/course-video-hls.service.ts +966 -0
  242. package/src/course/course.controller.ts +33 -19
  243. package/src/course/course.mcp-tools.ts +1 -1
  244. package/src/course/course.module.ts +16 -0
  245. package/src/course/course.service.ts +119 -61
  246. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  247. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  248. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  249. package/src/course/dto/create-course-export.dto.ts +56 -0
  250. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  251. package/src/course/ffmpeg.util.ts +65 -0
  252. package/src/course/lms-bulk-upload-automation.service.ts +156 -6
  253. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  254. package/src/course/lms-bulk-upload.controller.ts +32 -2
  255. package/src/course/lms-bulk-upload.service.ts +70 -7
  256. package/src/course/lms-setting.controller.ts +4 -2
  257. package/src/course/scorm12-schemas.ts +9 -0
  258. package/src/enterprise/training/training-student.service.ts +221 -2
  259. package/src/evaluation/evaluation.service.ts +123 -0
  260. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  261. package/src/exam/exam.module.ts +2 -1
  262. package/src/exam/exam.service.ts +86 -0
  263. package/src/exam/question.controller.ts +28 -0
  264. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  265. package/src/lms-commerce-access.subscriber.ts +88 -0
  266. package/src/lms.module.ts +16 -5
  267. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  268. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  269. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  270. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  271. package/src/platforma/platforma-performance.service.ts +606 -0
  272. package/src/platforma/platforma-search.service.ts +48 -0
  273. package/src/platforma/platforma-video.service.ts +346 -0
  274. package/src/platforma/platforma.controller.ts +137 -1
  275. package/src/platforma/platforma.service.ts +268 -268
  276. package/src/realtime/lms-realtime.controller.ts +27 -1
  277. package/src/realtime/lms-realtime.service.ts +2 -1
  278. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  279. package/src/student-xp/student-xp.controller.ts +18 -2
  280. package/src/student-xp/student-xp.service.ts +84 -2
  281. package/hedhog/data/video_resolution_profile.yaml +0 -7
  282. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  283. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  284. package/hedhog/table/video_resolution_profile.yaml +0 -18
  285. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  286. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  287. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  288. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  289. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  290. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -31,7 +31,6 @@ import { toast } from 'sonner';
31
31
  import { z } from 'zod';
32
32
 
33
33
  import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
34
- import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
35
34
  import { FileTypeIcon } from '@/components/file-type-icon';
36
35
  import { RichTextEditor } from '@/components/rich-text-editor';
37
36
  import {
@@ -46,6 +45,15 @@ import {
46
45
  } from '@/components/ui/alert-dialog';
47
46
  import { Badge } from '@/components/ui/badge';
48
47
  import { Button } from '@/components/ui/button';
48
+ import { Checkbox } from '@/components/ui/checkbox';
49
+ import {
50
+ Dialog,
51
+ DialogContent,
52
+ DialogDescription,
53
+ DialogFooter,
54
+ DialogHeader,
55
+ DialogTitle,
56
+ } from '@/components/ui/dialog';
49
57
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
50
58
  import {
51
59
  DropdownMenu,
@@ -108,6 +116,8 @@ import {
108
116
  import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
109
117
  import { courseStructureQueryKey } from '../_data/use-course-structure-query';
110
118
  import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
119
+ import { CourseExportSheet } from './course-export-sheet';
120
+ import { CourseExportsTab } from './course-exports-tab';
111
121
  import { CourseOperationsTab } from './course-operations-tab';
112
122
  import { CourseOverviewTab } from './course-overview-tab';
113
123
  import { CourseXpOverviewTab } from './course-xp-overview-tab';
@@ -190,13 +200,6 @@ type ApiQueuedDeleteResponse = {
190
200
  >;
191
201
  };
192
202
 
193
- type VideoProfileOption = {
194
- id: number;
195
- name: string;
196
- ffmpeg_params: string;
197
- status: string;
198
- };
199
-
200
203
  type ApiCategory = { id: number; slug: string; name: string };
201
204
  type ApiCategoryDetail = {
202
205
  id: number;
@@ -498,7 +501,25 @@ function EditorCourseSkeleton() {
498
501
 
499
502
  // ── Component ─────────────────────────────────────────────────────────────────
500
503
 
501
- export function EditorCourse() {
504
+ const COURSE_TABS = [
505
+ 'visao-geral',
506
+ 'detalhes',
507
+ 'xp',
508
+ 'midia',
509
+ 'recursos',
510
+ 'operacoes',
511
+ 'publicacao',
512
+ 'extra',
513
+ 'exportacoes',
514
+ ] as const;
515
+ type CourseTab = (typeof COURSE_TABS)[number];
516
+
517
+ interface EditorCourseProps {
518
+ defaultTab?: string;
519
+ onTabChange?: (tab: string) => void;
520
+ }
521
+
522
+ export function EditorCourse({ defaultTab, onTabChange }: EditorCourseProps) {
502
523
  const courseId = useStructureStore((s) => s.courseId);
503
524
  const course = useStructureStore((s) => s.course);
504
525
  const sessions = useStructureStore((s) => s.sessions);
@@ -518,9 +539,21 @@ export function EditorCourse() {
518
539
  const lmsSettings = useLmsSettingsQuery();
519
540
 
520
541
  // ── UI state ────────────────────────────────────────────────────────────────
521
- const [activeTab, setActiveTab] = useState('visao-geral');
522
- const [pendingBulkJob, setPendingBulkJob] = useState<'transcription' | 'xp_recalculation' | null>(null);
542
+ const [activeTab, setActiveTab] = useState<CourseTab>(() =>
543
+ defaultTab && (COURSE_TABS as readonly string[]).includes(defaultTab)
544
+ ? (defaultTab as CourseTab)
545
+ : 'visao-geral'
546
+ );
547
+ const [exportSheetOpen, setExportSheetOpen] = useState(false);
548
+ const [pendingBulkJob, setPendingBulkJob] = useState<'transcription' | 'xp_recalculation' | 'video_processing' | null>(null);
523
549
  const [isBulkJobPending, setIsBulkJobPending] = useState(false);
550
+ const [videoProcessingStats, setVideoProcessingStats] = useState<{
551
+ withOriginal: number;
552
+ withoutOriginal: number;
553
+ alreadyProcessed: number;
554
+ } | null>(null);
555
+ const [isFetchingVideoStats, setIsFetchingVideoStats] = useState(false);
556
+ const [reprocessAlreadyProcessed, setReprocessAlreadyProcessed] = useState(false);
524
557
  const [categoryEditSheetOpen, setCategoryEditSheetOpen] = useState(false);
525
558
  const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
526
559
  null
@@ -569,18 +602,6 @@ export function EditorCourse() {
569
602
  const [isUploadingResources, setIsUploadingResources] = useState(false);
570
603
  const [isSavingResources, setIsSavingResources] = useState(false);
571
604
  const resourcesInputRef = useRef<HTMLInputElement>(null);
572
- const [linkedProfileIds, setLinkedProfileIds] = useState<number[]>([]);
573
- const [videoProfilePickerResetKey, setVideoProfilePickerResetKey] =
574
- useState(0);
575
- const [videoProfileEditSheetOpen, setVideoProfileEditSheetOpen] =
576
- useState(false);
577
- const [editingVideoProfileId, setEditingVideoProfileId] = useState<
578
- number | null
579
- >(null);
580
- const [editingVideoProfileName, setEditingVideoProfileName] = useState('');
581
- const [editingVideoProfileParams, setEditingVideoProfileParams] =
582
- useState('');
583
- const [savingVideoProfileEdit, setSavingVideoProfileEdit] = useState(false);
584
605
 
585
606
  // ── Queries ─────────────────────────────────────────────────────────────────
586
607
  const {
@@ -668,22 +689,6 @@ export function EditorCourse() {
668
689
  initialData: { data: [] },
669
690
  });
670
691
 
671
- const {
672
- data: allVideoProfiles = [],
673
- refetch: refetchVideoProfiles,
674
- isFetching: isFetchingVideoProfiles,
675
- } = useQuery<VideoProfileOption[]>({
676
- queryKey: ['lms-video-resolution-profiles-all'],
677
- queryFn: async () => {
678
- const response = await request<VideoProfileOption[]>({
679
- url: '/lms/video-resolution-profiles/all',
680
- method: 'GET',
681
- });
682
- return response.data;
683
- },
684
- initialData: [],
685
- });
686
-
687
692
  const { data: operationsConfig } = useQuery<{ isAvailable: boolean }>({
688
693
  queryKey: ['lms-course-operations-config', courseId],
689
694
  enabled: Boolean(courseId),
@@ -745,15 +750,6 @@ export function EditorCourse() {
745
750
  return data;
746
751
  },
747
752
  onSuccess: (data) => {
748
- if (courseId) {
749
- void request({
750
- url: `/lms/courses/${courseId}/video-resolution-profiles/sync`,
751
- method: 'POST',
752
- data: { profileIds: linkedProfileIds },
753
- }).catch(() => {
754
- toast.error('Erro ao sincronizar perfis de vídeo.');
755
- });
756
- }
757
753
  setPersistedCertificateModel(data.modeloCertificado || '');
758
754
  updateCourseInStore({
759
755
  code: data.code,
@@ -897,14 +893,6 @@ export function EditorCourse() {
897
893
  );
898
894
  }, [certificateTemplateData, createdTemplateOptions]);
899
895
 
900
- const availableVideoProfiles = useMemo(
901
- () =>
902
- allVideoProfiles.filter(
903
- (profile) => !linkedProfileIds.includes(profile.id)
904
- ),
905
- [allVideoProfiles, linkedProfileIds]
906
- );
907
-
908
896
  // ── Structural stats ─────────────────────────────────────────────────────────
909
897
  const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
910
898
  const hours = Math.floor(totalMinutes / 60);
@@ -915,9 +903,6 @@ export function EditorCourse() {
915
903
  ).length;
916
904
 
917
905
  const formValues = form.watch();
918
- const showVideoTab =
919
- formValues.tipoOferta === 'sob_demanda' ||
920
- formValues.tipoOferta === 'hibrido';
921
906
  const descriptionText = String(formValues.descricaoPublica ?? '')
922
907
  .replace(/<[^>]+>/g, '')
923
908
  .trim();
@@ -1049,32 +1034,6 @@ export function EditorCourse() {
1049
1034
  };
1050
1035
  }, [courseId, request]);
1051
1036
 
1052
- useEffect(() => {
1053
- if (!showVideoTab && activeTab === 'videos') {
1054
- setActiveTab('detalhes');
1055
- }
1056
- }, [showVideoTab, activeTab]);
1057
-
1058
- useEffect(() => {
1059
- if (!courseId) return;
1060
- let active = true;
1061
- void request<VideoProfileOption[]>({
1062
- url: `/lms/courses/${courseId}/video-resolution-profiles`,
1063
- method: 'GET',
1064
- })
1065
- .then((res) => {
1066
- if (!active) return;
1067
- setLinkedProfileIds((res.data ?? []).map((p) => p.id));
1068
- })
1069
- .catch(() => {
1070
- if (!active) return;
1071
- setLinkedProfileIds([]);
1072
- });
1073
- return () => {
1074
- active = false;
1075
- };
1076
- }, [courseId, request]);
1077
-
1078
1037
  async function syncCourseResources(nextResources: CourseResourceItem[]) {
1079
1038
  setIsSavingResources(true);
1080
1039
  try {
@@ -1394,38 +1353,6 @@ export function EditorCourse() {
1394
1353
  }
1395
1354
  }
1396
1355
 
1397
- function handleEditVideoProfile(profileId: number) {
1398
- const profile = allVideoProfiles.find((p) => p.id === profileId);
1399
- if (!profile) return;
1400
- setEditingVideoProfileId(profileId);
1401
- setEditingVideoProfileName(profile.name);
1402
- setEditingVideoProfileParams(profile.ffmpeg_params);
1403
- setVideoProfileEditSheetOpen(true);
1404
- }
1405
-
1406
- async function handleSaveVideoProfileEdit() {
1407
- if (!editingVideoProfileId) return;
1408
- try {
1409
- setSavingVideoProfileEdit(true);
1410
- await request({
1411
- url: `/lms/video-resolution-profiles/${editingVideoProfileId}`,
1412
- method: 'PATCH',
1413
- data: {
1414
- name: editingVideoProfileName,
1415
- ffmpeg_params: editingVideoProfileParams,
1416
- },
1417
- });
1418
- await refetchVideoProfiles();
1419
- setVideoProfileEditSheetOpen(false);
1420
- setEditingVideoProfileId(null);
1421
- toast.success('Perfil de vídeo atualizado com sucesso.');
1422
- } catch {
1423
- toast.error('Erro ao atualizar o perfil de vídeo.');
1424
- } finally {
1425
- setSavingVideoProfileEdit(false);
1426
- }
1427
- }
1428
-
1429
1356
  async function handleEditCategory(categorySlug: string) {
1430
1357
  const category = (categoryListData?.data ?? []).find(
1431
1358
  (item) => item.slug === categorySlug
@@ -1648,52 +1575,30 @@ export function EditorCourse() {
1648
1575
  }
1649
1576
 
1650
1577
  function onSubmit(data: CourseEditFormValues) {
1651
- if (data.status === 'publicado') {
1652
- const requiredProfileIds = new Set(linkedProfileIds);
1653
- const invalidLesson = lessons.find((lesson) => {
1654
- if (
1655
- lesson.type !== 'video' ||
1656
- lesson.videoProvider !== 'file_storage'
1657
- ) {
1658
- return false;
1659
- }
1660
-
1661
- if (requiredProfileIds.size === 0) {
1662
- return true;
1663
- }
1664
-
1665
- const resourceTypes = new Set(lesson.resources.map((res) => res.type));
1666
- return [...requiredProfileIds].some(
1667
- (profileId) => !resourceTypes.has(`video_profile:${profileId}`)
1668
- );
1669
- });
1670
-
1671
- if (invalidLesson) {
1672
- toast.error(
1673
- linkedProfileIds.length === 0
1674
- ? 'Configure ao menos um perfil de vídeo antes de publicar o curso.'
1675
- : `A aula "${invalidLesson.title}" precisa de um vídeo para cada perfil antes da publicação.`
1676
- );
1677
- return;
1678
- }
1679
- }
1680
-
1681
1578
  saveCourse(data);
1682
1579
  }
1683
1580
 
1581
+ function handlePublish() {
1582
+ const values = form.getValues();
1583
+ saveCourse({ ...values, status: 'publicado' });
1584
+ }
1585
+
1684
1586
  function onFormError(errors: Record<string, unknown>) {
1685
1587
  const first = Object.values(errors)[0] as { message?: string } | undefined;
1686
1588
  toast.error(first?.message ?? 'Verifique os campos obrigatórios');
1687
1589
  }
1688
1590
 
1689
1591
  // ── Bulk jobs ─────────────────────────────────────────────────────────────────
1690
- const handleBulkJob = async (jobType: 'transcription' | 'xp_recalculation') => {
1592
+ const handleBulkJob = async (
1593
+ jobType: 'transcription' | 'xp_recalculation' | 'video_processing',
1594
+ opts?: { reprocessAlreadyProcessed?: boolean },
1595
+ ) => {
1691
1596
  setIsBulkJobPending(true);
1692
1597
  try {
1693
1598
  const res = await request<{ queued: number; skipped: number }>({
1694
1599
  url: `/lms/courses/${courseId}/structure/bulk-jobs`,
1695
1600
  method: 'POST',
1696
- data: { jobType },
1601
+ data: { jobType, ...opts },
1697
1602
  });
1698
1603
  toast.success(`${res.data?.queued ?? 0} job(s) enfileirado(s), ${res.data?.skipped ?? 0} ignorado(s).`);
1699
1604
  } catch {
@@ -1701,6 +1606,28 @@ export function EditorCourse() {
1701
1606
  } finally {
1702
1607
  setIsBulkJobPending(false);
1703
1608
  setPendingBulkJob(null);
1609
+ setVideoProcessingStats(null);
1610
+ }
1611
+ };
1612
+
1613
+ const handleOpenVideoProcessingDialog = async () => {
1614
+ setIsFetchingVideoStats(true);
1615
+ setReprocessAlreadyProcessed(false);
1616
+ try {
1617
+ const res = await request<{
1618
+ withOriginal: number;
1619
+ withoutOriginal: number;
1620
+ alreadyProcessed: number;
1621
+ }>({
1622
+ url: `/lms/courses/${courseId}/structure/video-processing-stats`,
1623
+ method: 'GET',
1624
+ });
1625
+ setVideoProcessingStats(res.data ?? null);
1626
+ setPendingBulkJob('video_processing');
1627
+ } catch {
1628
+ toast.error('Erro ao buscar estatísticas de processamento de vídeo.');
1629
+ } finally {
1630
+ setIsFetchingVideoStats(false);
1704
1631
  }
1705
1632
  };
1706
1633
 
@@ -1839,9 +1766,9 @@ export function EditorCourse() {
1839
1766
  variant="ghost"
1840
1767
  size="icon"
1841
1768
  className="size-6 shrink-0"
1842
- disabled={isBulkJobPending}
1769
+ disabled={isBulkJobPending || isFetchingVideoStats}
1843
1770
  >
1844
- {isBulkJobPending ? (
1771
+ {isBulkJobPending || isFetchingVideoStats ? (
1845
1772
  <Loader2 className="size-3.5 animate-spin" />
1846
1773
  ) : (
1847
1774
  <Briefcase className="size-3.5" />
@@ -1867,11 +1794,27 @@ export function EditorCourse() {
1867
1794
  <Zap className="size-4 text-muted-foreground" />
1868
1795
  Recalcular XP de todas as aulas
1869
1796
  </DropdownMenuItem>
1797
+ <DropdownMenuItem
1798
+ onSelect={handleOpenVideoProcessingDialog}
1799
+ className="gap-2"
1800
+ >
1801
+ <Video className="size-4 text-muted-foreground" />
1802
+ Processar todos os vídeos
1803
+ </DropdownMenuItem>
1804
+ <DropdownMenuSeparator />
1805
+ <DropdownMenuLabel className="text-xs">Exportar</DropdownMenuLabel>
1806
+ <DropdownMenuItem
1807
+ onSelect={() => setExportSheetOpen(true)}
1808
+ className="gap-2"
1809
+ >
1810
+ <Download className="size-4 text-muted-foreground" />
1811
+ SCORM 1.2
1812
+ </DropdownMenuItem>
1870
1813
  </DropdownMenuContent>
1871
1814
  </DropdownMenu>
1872
1815
 
1873
1816
  <AlertDialog
1874
- open={pendingBulkJob !== null}
1817
+ open={pendingBulkJob === 'transcription' || pendingBulkJob === 'xp_recalculation'}
1875
1818
  onOpenChange={(open) => { if (!open) setPendingBulkJob(null); }}
1876
1819
  >
1877
1820
  <AlertDialogContent>
@@ -1890,20 +1833,81 @@ export function EditorCourse() {
1890
1833
  <AlertDialogFooter>
1891
1834
  <AlertDialogCancel>Cancelar</AlertDialogCancel>
1892
1835
  <AlertDialogAction
1893
- onClick={() => pendingBulkJob && handleBulkJob(pendingBulkJob)}
1836
+ onClick={() => pendingBulkJob && handleBulkJob(pendingBulkJob as 'transcription' | 'xp_recalculation')}
1894
1837
  >
1895
1838
  Confirmar
1896
1839
  </AlertDialogAction>
1897
1840
  </AlertDialogFooter>
1898
1841
  </AlertDialogContent>
1899
1842
  </AlertDialog>
1843
+
1844
+ <Dialog
1845
+ open={pendingBulkJob === 'video_processing'}
1846
+ onOpenChange={(open) => {
1847
+ if (!open) {
1848
+ setPendingBulkJob(null);
1849
+ setVideoProcessingStats(null);
1850
+ }
1851
+ }}
1852
+ >
1853
+ <DialogContent className="sm:max-w-md">
1854
+ <DialogHeader>
1855
+ <DialogTitle>Processar todos os vídeos?</DialogTitle>
1856
+ <DialogDescription>
1857
+ {videoProcessingStats ? (
1858
+ <>
1859
+ <span className="font-medium text-foreground">{videoProcessingStats.withOriginal}</span> aula(s) com vídeo original elegíveis para processamento.{' '}
1860
+ <span className="font-medium text-foreground">{videoProcessingStats.withoutOriginal}</span> aula(s) sem vídeo original serão ignoradas.
1861
+ </>
1862
+ ) : (
1863
+ 'Carregando estatísticas...'
1864
+ )}
1865
+ </DialogDescription>
1866
+ </DialogHeader>
1867
+ {videoProcessingStats && videoProcessingStats.alreadyProcessed > 0 && (
1868
+ <div className="flex items-center gap-2 py-1">
1869
+ <Checkbox
1870
+ id="reprocess-checkbox"
1871
+ checked={reprocessAlreadyProcessed}
1872
+ onCheckedChange={(checked) => setReprocessAlreadyProcessed(checked === true)}
1873
+ />
1874
+ <label htmlFor="reprocess-checkbox" className="text-sm leading-none cursor-pointer">
1875
+ Reprocessar aulas que já foram processadas{' '}
1876
+ <span className="text-muted-foreground">({videoProcessingStats.alreadyProcessed} aula(s))</span>
1877
+ </label>
1878
+ </div>
1879
+ )}
1880
+ <DialogFooter>
1881
+ <Button
1882
+ variant="outline"
1883
+ onClick={() => {
1884
+ setPendingBulkJob(null);
1885
+ setVideoProcessingStats(null);
1886
+ }}
1887
+ >
1888
+ Cancelar
1889
+ </Button>
1890
+ <Button
1891
+ disabled={isBulkJobPending || !videoProcessingStats}
1892
+ onClick={() => handleBulkJob('video_processing', { reprocessAlreadyProcessed })}
1893
+ >
1894
+ {isBulkJobPending && <Loader2 className="size-4 animate-spin mr-2" />}
1895
+ Confirmar
1896
+ </Button>
1897
+ </DialogFooter>
1898
+ </DialogContent>
1899
+ </Dialog>
1900
1900
  </div>
1901
1901
  </div>
1902
1902
 
1903
1903
  {/* ── Tabs ─────────────────────────────────────────────────────────── */}
1904
1904
  <Tabs
1905
1905
  value={activeTab}
1906
- onValueChange={setActiveTab}
1906
+ onValueChange={(v) => {
1907
+ const tab = v as CourseTab;
1908
+ setActiveTab(tab);
1909
+ onTabChange?.(tab);
1910
+ }}
1907
1911
  className="flex flex-1 min-h-0 min-w-0 flex-col"
1908
1912
  >
1909
1913
  <TabsList
@@ -1922,10 +1926,10 @@ export function EditorCourse() {
1922
1926
  'xp',
1923
1927
  'midia',
1924
1928
  'recursos',
1925
- ...(showVideoTab ? ['videos'] : []),
1926
1929
  ...(showOperationsTab ? ['operacoes'] : []),
1927
1930
  'publicacao',
1928
1931
  'extra',
1932
+ 'exportacoes',
1929
1933
  ] as const
1930
1934
  ).map((tab) => (
1931
1935
  <TabsTrigger
@@ -1950,11 +1954,11 @@ export function EditorCourse() {
1950
1954
  ? t('structureEditor.tabs.resources')
1951
1955
  : tab === 'extra'
1952
1956
  ? t('structureEditor.tabs.extra')
1953
- : tab === 'videos'
1954
- ? t('structureEditor.tabs.videoProfiles')
1955
- : tab === 'operacoes'
1957
+ : tab === 'operacoes'
1956
1958
  ? 'Operações'
1957
- : t('structureEditor.tabs.publish')}
1959
+ : tab === 'exportacoes'
1960
+ ? 'Exportações'
1961
+ : t('structureEditor.tabs.publish')}
1958
1962
  </TabsTrigger>
1959
1963
  ))}
1960
1964
  </TabsList>
@@ -2485,174 +2489,6 @@ export function EditorCourse() {
2485
2489
  />
2486
2490
  </TabsContent>
2487
2491
 
2488
- {/* ── Tab: Vídeos (on_demand / blended only) ─────────────── */}
2489
- {showVideoTab && (
2490
- <TabsContent
2491
- value="videos"
2492
- className="mt-0 flex min-w-0 flex-col gap-3"
2493
- >
2494
- <Card className="bg-muted/20 py-2 gap-2">
2495
- <CardHeader className="px-3 pt-2 pb-0">
2496
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
2497
- {t('structureEditor.videoProfiles.title')}
2498
- </CardTitle>
2499
- </CardHeader>
2500
- <CardContent className="px-3 pb-2 flex flex-col gap-2">
2501
- <EntityPicker
2502
- key={videoProfilePickerResetKey}
2503
- placeholder={t(
2504
- 'structureEditor.videoProfiles.placeholder'
2505
- )}
2506
- options={availableVideoProfiles}
2507
- getOptionValue={(opt) => opt.id}
2508
- getOptionLabel={(opt) => opt.name}
2509
- onChange={(_val, opt) => {
2510
- if (opt && !linkedProfileIds.includes(opt.id)) {
2511
- setLinkedProfileIds((prev) => [...prev, opt.id]);
2512
- setVideoProfilePickerResetKey(
2513
- (current) => current + 1
2514
- );
2515
- }
2516
- }}
2517
- showCreateButton
2518
- createTitle={t(
2519
- 'structureEditor.videoProfiles.createTitle'
2520
- )}
2521
- createFields={[
2522
- {
2523
- name: 'name',
2524
- label: t(
2525
- 'structureEditor.videoProfiles.createFields.name.label'
2526
- ),
2527
- placeholder: t(
2528
- 'structureEditor.videoProfiles.createFields.name.placeholder'
2529
- ),
2530
- required: true,
2531
- },
2532
- {
2533
- name: 'ffmpeg_params',
2534
- label: t(
2535
- 'structureEditor.videoProfiles.createFields.ffmpegParams.label'
2536
- ),
2537
- placeholder: t(
2538
- 'structureEditor.videoProfiles.createFields.ffmpegParams.placeholder'
2539
- ),
2540
- required: true,
2541
- },
2542
- ]}
2543
- onCreate={async (values) => {
2544
- try {
2545
- const response = await request<VideoProfileOption>({
2546
- url: '/lms/video-resolution-profiles',
2547
- method: 'POST',
2548
- data: {
2549
- name: values.name,
2550
- ffmpeg_params: values.ffmpeg_params,
2551
- },
2552
- });
2553
- await refetchVideoProfiles();
2554
- return response.data;
2555
- } catch {
2556
- toast.error(
2557
- t('structureEditor.videoProfiles.createError')
2558
- );
2559
- return null;
2560
- }
2561
- }}
2562
- />
2563
-
2564
- {isFetchingVideoProfiles ? (
2565
- <div className="flex flex-col gap-1">
2566
- {Array.from({ length: 3 }).map((_, index) => (
2567
- <div
2568
- key={`video-profile-skeleton-${index}`}
2569
- className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
2570
- >
2571
- <Skeleton className="size-3.5 rounded-full shrink-0" />
2572
- <Skeleton className="h-3 w-full max-w-56" />
2573
- </div>
2574
- ))}
2575
- </div>
2576
- ) : linkedProfileIds.length === 0 ? (
2577
- <p className="text-center text-xs text-muted-foreground py-1">
2578
- {t('structureEditor.videoProfiles.empty')}
2579
- </p>
2580
- ) : (
2581
- <div className="flex flex-col gap-1">
2582
- {linkedProfileIds.map((profileId) => {
2583
- const profile = allVideoProfiles.find(
2584
- (p) => p.id === profileId
2585
- );
2586
- return (
2587
- <div
2588
- key={profileId}
2589
- className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
2590
- >
2591
- <Video className="size-3.5 shrink-0 text-violet-500" />
2592
- <div className="flex-1 min-w-0">
2593
- <p className="text-xs font-medium truncate">
2594
- {profile?.name?.trim()
2595
- ? profile.name
2596
- : t(
2597
- 'structureEditor.videoProfiles.fallbackName',
2598
- {
2599
- id: profileId,
2600
- }
2601
- )}
2602
- </p>
2603
- </div>
2604
- <IconActionTooltip
2605
- label={t(
2606
- 'structureEditor.videoProfiles.editAria'
2607
- )}
2608
- >
2609
- <Button
2610
- type="button"
2611
- variant="ghost"
2612
- size="icon"
2613
- className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
2614
- onClick={() =>
2615
- handleEditVideoProfile(profileId)
2616
- }
2617
- aria-label={t(
2618
- 'structureEditor.videoProfiles.editAria'
2619
- )}
2620
- >
2621
- <Pencil className="size-3" />
2622
- </Button>
2623
- </IconActionTooltip>
2624
- <IconActionTooltip
2625
- label={t(
2626
- 'structureEditor.videoProfiles.removeAria'
2627
- )}
2628
- >
2629
- <Button
2630
- type="button"
2631
- variant="ghost"
2632
- size="icon"
2633
- className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
2634
- onClick={() =>
2635
- setLinkedProfileIds((prev) =>
2636
- prev.filter((id) => id !== profileId)
2637
- )
2638
- }
2639
- aria-label={t(
2640
- 'structureEditor.videoProfiles.removeAria'
2641
- )}
2642
- >
2643
- <X className="size-3" />
2644
- </Button>
2645
- </IconActionTooltip>
2646
- </div>
2647
- );
2648
- })}
2649
- </div>
2650
- )}
2651
- </CardContent>
2652
- </Card>
2653
- </TabsContent>
2654
- )}
2655
-
2656
2492
  {/* ── Tab: Operações ──────────────────────────────────────── */}
2657
2493
  {showOperationsTab && (
2658
2494
  <TabsContent value="operacoes" className="mt-0 min-w-0">
@@ -2715,21 +2551,39 @@ export function EditorCourse() {
2715
2551
  {t('structureEditor.publishChecklist.statusTitle')}
2716
2552
  </CardTitle>
2717
2553
  </CardHeader>
2718
- <CardContent className="px-3 pb-2 flex items-center justify-between gap-2">
2719
- <p className="text-xs text-muted-foreground">
2720
- {t('structureEditor.publishChecklist.statusProgress', {
2721
- completed: completedRequired,
2722
- total: requiredChecklist.length,
2723
- })}
2724
- </p>
2725
- <Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
2726
- {isReadyToPublish
2727
- ? t('structureEditor.publishChecklist.ready')
2728
- : t('structureEditor.publishChecklist.notReady')}
2729
- </Badge>
2554
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
2555
+ <div className="flex items-center justify-between gap-2">
2556
+ <p className="text-xs text-muted-foreground">
2557
+ {t('structureEditor.publishChecklist.statusProgress', {
2558
+ completed: completedRequired,
2559
+ total: requiredChecklist.length,
2560
+ })}
2561
+ </p>
2562
+ <Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
2563
+ {isReadyToPublish
2564
+ ? t('structureEditor.publishChecklist.ready')
2565
+ : t('structureEditor.publishChecklist.notReady')}
2566
+ </Badge>
2567
+ </div>
2568
+ <Button
2569
+ type="button"
2570
+ size="sm"
2571
+ className="w-full h-8 text-xs"
2572
+ disabled={!isReadyToPublish || formValues.status === 'publicado' || saving}
2573
+ onClick={handlePublish}
2574
+ >
2575
+ {formValues.status === 'publicado'
2576
+ ? t('structureEditor.publishChecklist.alreadyPublished')
2577
+ : t('structureEditor.publishChecklist.publishButton')}
2578
+ </Button>
2730
2579
  </CardContent>
2731
2580
  </Card>
2732
2581
  </TabsContent>
2582
+
2583
+ {/* ── Tab: Exportações ────────────────────────────────────────── */}
2584
+ <TabsContent value="exportacoes" className="mt-0 min-w-0">
2585
+ <CourseExportsTab courseId={courseId} />
2586
+ </TabsContent>
2733
2587
  </div>
2734
2588
  </div>
2735
2589
  </Tabs>
@@ -2778,68 +2632,6 @@ export function EditorCourse() {
2778
2632
  </div>
2779
2633
  </form>
2780
2634
 
2781
- <Sheet
2782
- open={videoProfileEditSheetOpen}
2783
- onOpenChange={(open) => {
2784
- setVideoProfileEditSheetOpen(open);
2785
- if (!open) setEditingVideoProfileId(null);
2786
- }}
2787
- >
2788
- <ResizableSheetContent
2789
- sheetId="lms-course-structure-video-profile-edit-sheet"
2790
- defaultWidth={560}
2791
- minWidth={420}
2792
- maxWidth={920}
2793
- className="sm:max-w-lg"
2794
- >
2795
- <SheetHeader>
2796
- <SheetTitle>
2797
- {t('structureEditor.videoProfiles.sheet.title')}
2798
- </SheetTitle>
2799
- <SheetDescription>
2800
- {t('structureEditor.videoProfiles.sheet.description')}
2801
- </SheetDescription>
2802
- </SheetHeader>
2803
- <div className="space-y-3 px-4 pb-4 pt-2">
2804
- <div className="space-y-1.5">
2805
- <FormLabel>
2806
- {t('structureEditor.videoProfiles.createFields.name.label')}
2807
- </FormLabel>
2808
- <Input
2809
- value={editingVideoProfileName}
2810
- onChange={(e) => setEditingVideoProfileName(e.target.value)}
2811
- placeholder={t(
2812
- 'structureEditor.videoProfiles.createFields.name.placeholder'
2813
- )}
2814
- />
2815
- </div>
2816
- <FfmpegParamsEditor
2817
- value={editingVideoProfileParams}
2818
- onChange={setEditingVideoProfileParams}
2819
- />
2820
- <div className="flex justify-end gap-2 pt-2">
2821
- <Button
2822
- type="button"
2823
- variant="outline"
2824
- onClick={() => setVideoProfileEditSheetOpen(false)}
2825
- disabled={savingVideoProfileEdit}
2826
- >
2827
- {t('structureEditor.videoProfiles.sheet.actions.cancel')}
2828
- </Button>
2829
- <Button
2830
- type="button"
2831
- disabled={savingVideoProfileEdit}
2832
- onClick={() => void handleSaveVideoProfileEdit()}
2833
- >
2834
- {savingVideoProfileEdit
2835
- ? t('structureEditor.videoProfiles.sheet.actions.saving')
2836
- : t('structureEditor.videoProfiles.sheet.actions.save')}
2837
- </Button>
2838
- </div>
2839
- </div>
2840
- </ResizableSheetContent>
2841
- </Sheet>
2842
-
2843
2635
  <Sheet
2844
2636
  open={categoryEditSheetOpen}
2845
2637
  onOpenChange={(open) => {
@@ -2915,6 +2707,14 @@ export function EditorCourse() {
2915
2707
  </ResizableSheetContent>
2916
2708
  </Sheet>
2917
2709
 
2710
+ <CourseExportSheet
2711
+ open={exportSheetOpen}
2712
+ onOpenChange={setExportSheetOpen}
2713
+ courseId={courseId}
2714
+ sessionCount={sessions.length}
2715
+ videoLessonCount={lessons.filter((l) => l.type === 'video').length}
2716
+ />
2717
+
2918
2718
  <CourseDeleteDialog
2919
2719
  open={deleteDialogOpen}
2920
2720
  onOpenChange={(open) => {