@hed-hog/lms 0.0.349 → 0.0.351

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 (496) hide show
  1. package/dist/achievement/achievement.controller.d.ts +62 -0
  2. package/dist/achievement/achievement.controller.d.ts.map +1 -0
  3. package/dist/achievement/achievement.controller.js +90 -0
  4. package/dist/achievement/achievement.controller.js.map +1 -0
  5. package/dist/achievement/achievement.mcp-tools.d.ts +19 -0
  6. package/dist/achievement/achievement.mcp-tools.d.ts.map +1 -0
  7. package/dist/achievement/achievement.mcp-tools.js +157 -0
  8. package/dist/achievement/achievement.mcp-tools.js.map +1 -0
  9. package/dist/achievement/achievement.module.d.ts +3 -0
  10. package/dist/achievement/achievement.module.d.ts.map +1 -0
  11. package/dist/achievement/achievement.module.js +26 -0
  12. package/dist/achievement/achievement.module.js.map +1 -0
  13. package/dist/achievement/achievement.service.d.ts +72 -0
  14. package/dist/achievement/achievement.service.d.ts.map +1 -0
  15. package/dist/achievement/achievement.service.js +200 -0
  16. package/dist/achievement/achievement.service.js.map +1 -0
  17. package/dist/achievement/dto/create-achievement.dto.d.ts +12 -0
  18. package/dist/achievement/dto/create-achievement.dto.d.ts.map +1 -0
  19. package/dist/achievement/dto/create-achievement.dto.js +60 -0
  20. package/dist/achievement/dto/create-achievement.dto.js.map +1 -0
  21. package/dist/achievement/dto/update-achievement.dto.d.ts +11 -0
  22. package/dist/achievement/dto/update-achievement.dto.d.ts.map +1 -0
  23. package/dist/achievement/dto/update-achievement.dto.js +57 -0
  24. package/dist/achievement/dto/update-achievement.dto.js.map +1 -0
  25. package/dist/bitcode-wallet/bitcode-wallet.controller.d.ts +114 -0
  26. package/dist/bitcode-wallet/bitcode-wallet.controller.d.ts.map +1 -0
  27. package/dist/bitcode-wallet/bitcode-wallet.controller.js +102 -0
  28. package/dist/bitcode-wallet/bitcode-wallet.controller.js.map +1 -0
  29. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.d.ts +25 -0
  30. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.d.ts.map +1 -0
  31. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.js +160 -0
  32. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.js.map +1 -0
  33. package/dist/bitcode-wallet/bitcode-wallet.module.d.ts +3 -0
  34. package/dist/bitcode-wallet/bitcode-wallet.module.d.ts.map +1 -0
  35. package/dist/bitcode-wallet/bitcode-wallet.module.js +26 -0
  36. package/dist/bitcode-wallet/bitcode-wallet.module.js.map +1 -0
  37. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +127 -0
  38. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -0
  39. package/dist/bitcode-wallet/bitcode-wallet.service.js +264 -0
  40. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -0
  41. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.d.ts +8 -0
  42. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  43. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.js +33 -0
  44. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.js.map +1 -0
  45. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.d.ts +4 -0
  46. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.d.ts.map +1 -0
  47. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.js +22 -0
  48. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.js.map +1 -0
  49. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.d.ts +7 -0
  50. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  51. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.js +35 -0
  52. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.js.map +1 -0
  53. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.d.ts +4 -0
  54. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.d.ts.map +1 -0
  55. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.js +23 -0
  56. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.js.map +1 -0
  57. package/dist/certificate/certificate.controller.d.ts +24 -2
  58. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  59. package/dist/certificate/certificate.controller.js +20 -6
  60. package/dist/certificate/certificate.controller.js.map +1 -1
  61. package/dist/certificate/certificate.mcp-tools.d.ts +24 -0
  62. package/dist/certificate/certificate.mcp-tools.d.ts.map +1 -0
  63. package/dist/certificate/certificate.mcp-tools.js +188 -0
  64. package/dist/certificate/certificate.mcp-tools.js.map +1 -0
  65. package/dist/certificate/certificate.module.d.ts.map +1 -1
  66. package/dist/certificate/certificate.module.js +2 -1
  67. package/dist/certificate/certificate.module.js.map +1 -1
  68. package/dist/certificate/certificate.service.d.ts +30 -4
  69. package/dist/certificate/certificate.service.d.ts.map +1 -1
  70. package/dist/certificate/certificate.service.js +157 -8
  71. package/dist/certificate/certificate.service.js.map +1 -1
  72. package/dist/certificate/dto/update-certificate-public-access.dto.d.ts +4 -0
  73. package/dist/certificate/dto/update-certificate-public-access.dto.d.ts.map +1 -0
  74. package/dist/certificate/dto/update-certificate-public-access.dto.js +21 -0
  75. package/dist/certificate/dto/update-certificate-public-access.dto.js.map +1 -0
  76. package/dist/class-group/class-group.mcp-tools.d.ts +87 -0
  77. package/dist/class-group/class-group.mcp-tools.d.ts.map +1 -0
  78. package/dist/class-group/class-group.mcp-tools.js +553 -0
  79. package/dist/class-group/class-group.mcp-tools.js.map +1 -0
  80. package/dist/class-group/class-group.module.d.ts.map +1 -1
  81. package/dist/class-group/class-group.module.js +2 -1
  82. package/dist/class-group/class-group.module.js.map +1 -1
  83. package/dist/class-group/class-group.service.d.ts +3 -1
  84. package/dist/class-group/class-group.service.d.ts.map +1 -1
  85. package/dist/class-group/class-group.service.js +45 -2
  86. package/dist/class-group/class-group.service.js.map +1 -1
  87. package/dist/course/course-operations-integration.service.d.ts +40 -0
  88. package/dist/course/course-operations-integration.service.d.ts.map +1 -0
  89. package/dist/course/course-operations-integration.service.js +372 -0
  90. package/dist/course/course-operations-integration.service.js.map +1 -0
  91. package/dist/course/course-structure.controller.d.ts +67 -14
  92. package/dist/course/course-structure.controller.d.ts.map +1 -1
  93. package/dist/course/course-structure.controller.js +45 -2
  94. package/dist/course/course-structure.controller.js.map +1 -1
  95. package/dist/course/course-structure.service.d.ts +58 -9
  96. package/dist/course/course-structure.service.d.ts.map +1 -1
  97. package/dist/course/course-structure.service.js +260 -62
  98. package/dist/course/course-structure.service.js.map +1 -1
  99. package/dist/course/course-video-conversion.service.d.ts +37 -0
  100. package/dist/course/course-video-conversion.service.d.ts.map +1 -0
  101. package/dist/course/course-video-conversion.service.js +308 -0
  102. package/dist/course/course-video-conversion.service.js.map +1 -0
  103. package/dist/course/course.controller.d.ts +29 -0
  104. package/dist/course/course.controller.d.ts.map +1 -1
  105. package/dist/course/course.controller.js +23 -0
  106. package/dist/course/course.controller.js.map +1 -1
  107. package/dist/course/course.mcp-tools.d.ts +90 -0
  108. package/dist/course/course.mcp-tools.d.ts.map +1 -0
  109. package/dist/course/course.mcp-tools.js +520 -0
  110. package/dist/course/course.mcp-tools.js.map +1 -0
  111. package/dist/course/course.module.d.ts.map +1 -1
  112. package/dist/course/course.module.js +23 -3
  113. package/dist/course/course.module.js.map +1 -1
  114. package/dist/course/course.service.d.ts +30 -1
  115. package/dist/course/course.service.d.ts.map +1 -1
  116. package/dist/course/course.service.js +159 -70
  117. package/dist/course/course.service.js.map +1 -1
  118. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -1
  119. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  120. package/dist/course/dto/create-course-structure-lesson.dto.js +16 -2
  121. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  122. package/dist/course/dto/create-course.dto.d.ts +2 -0
  123. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  124. package/dist/course/dto/create-course.dto.js +16 -0
  125. package/dist/course/dto/create-course.dto.js.map +1 -1
  126. package/dist/course/dto/update-course-resources.dto.d.ts +11 -0
  127. package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -0
  128. package/dist/course/dto/update-course-resources.dto.js +51 -0
  129. package/dist/course/dto/update-course-resources.dto.js.map +1 -0
  130. package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts +23 -0
  131. package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts.map +1 -0
  132. package/dist/course-lesson-discussion/course-lesson-discussion.controller.js +78 -0
  133. package/dist/course-lesson-discussion/course-lesson-discussion.controller.js.map +1 -0
  134. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts +22 -0
  135. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts.map +1 -0
  136. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js +120 -0
  137. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js.map +1 -0
  138. package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts +3 -0
  139. package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts.map +1 -0
  140. package/dist/course-lesson-discussion/course-lesson-discussion.module.js +26 -0
  141. package/dist/course-lesson-discussion/course-lesson-discussion.module.js.map +1 -0
  142. package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts +49 -0
  143. package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts.map +1 -0
  144. package/dist/course-lesson-discussion/course-lesson-discussion.service.js +272 -0
  145. package/dist/course-lesson-discussion/course-lesson-discussion.service.js.map +1 -0
  146. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts +6 -0
  147. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts.map +1 -0
  148. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js +33 -0
  149. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js.map +1 -0
  150. package/dist/course-lesson-note/course-lesson-note.controller.d.ts +53 -0
  151. package/dist/course-lesson-note/course-lesson-note.controller.d.ts.map +1 -0
  152. package/dist/course-lesson-note/course-lesson-note.controller.js +93 -0
  153. package/dist/course-lesson-note/course-lesson-note.controller.js.map +1 -0
  154. package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts +27 -0
  155. package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts.map +1 -0
  156. package/dist/course-lesson-note/course-lesson-note.mcp-tools.js +145 -0
  157. package/dist/course-lesson-note/course-lesson-note.mcp-tools.js.map +1 -0
  158. package/dist/course-lesson-note/course-lesson-note.module.d.ts +3 -0
  159. package/dist/course-lesson-note/course-lesson-note.module.d.ts.map +1 -0
  160. package/dist/course-lesson-note/course-lesson-note.module.js +26 -0
  161. package/dist/course-lesson-note/course-lesson-note.module.js.map +1 -0
  162. package/dist/course-lesson-note/course-lesson-note.service.d.ts +59 -0
  163. package/dist/course-lesson-note/course-lesson-note.service.d.ts.map +1 -0
  164. package/dist/course-lesson-note/course-lesson-note.service.js +195 -0
  165. package/dist/course-lesson-note/course-lesson-note.service.js.map +1 -0
  166. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts +6 -0
  167. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts.map +1 -0
  168. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js +33 -0
  169. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js.map +1 -0
  170. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts +6 -0
  171. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts.map +1 -0
  172. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js +35 -0
  173. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js.map +1 -0
  174. package/dist/dashboard/dashboard.mcp-tools.d.ts +10 -0
  175. package/dist/dashboard/dashboard.mcp-tools.d.ts.map +1 -0
  176. package/dist/dashboard/dashboard.mcp-tools.js +46 -0
  177. package/dist/dashboard/dashboard.mcp-tools.js.map +1 -0
  178. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  179. package/dist/dashboard/dashboard.module.js +2 -1
  180. package/dist/dashboard/dashboard.module.js.map +1 -1
  181. package/dist/enterprise/enterprise.controller.d.ts +3 -3
  182. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  183. package/dist/enterprise/enterprise.controller.js +0 -1
  184. package/dist/enterprise/enterprise.controller.js.map +1 -1
  185. package/dist/enterprise/enterprise.mcp-tools.d.ts +82 -0
  186. package/dist/enterprise/enterprise.mcp-tools.d.ts.map +1 -0
  187. package/dist/enterprise/enterprise.mcp-tools.js +516 -0
  188. package/dist/enterprise/enterprise.mcp-tools.js.map +1 -0
  189. package/dist/enterprise/enterprise.module.d.ts.map +1 -1
  190. package/dist/enterprise/enterprise.module.js +2 -1
  191. package/dist/enterprise/enterprise.module.js.map +1 -1
  192. package/dist/enterprise/enterprise.service.d.ts +3 -3
  193. package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -1
  194. package/dist/enterprise/training/enterprise-training.module.js +11 -1
  195. package/dist/enterprise/training/enterprise-training.module.js.map +1 -1
  196. package/dist/enterprise/training/training-admin.mcp-tools.d.ts +79 -0
  197. package/dist/enterprise/training/training-admin.mcp-tools.d.ts.map +1 -0
  198. package/dist/enterprise/training/training-admin.mcp-tools.js +620 -0
  199. package/dist/enterprise/training/training-admin.mcp-tools.js.map +1 -0
  200. package/dist/enterprise/training/training-instructor.mcp-tools.d.ts +47 -0
  201. package/dist/enterprise/training/training-instructor.mcp-tools.d.ts.map +1 -0
  202. package/dist/enterprise/training/training-instructor.mcp-tools.js +275 -0
  203. package/dist/enterprise/training/training-instructor.mcp-tools.js.map +1 -0
  204. package/dist/enterprise/training/training-student.controller.d.ts +24 -0
  205. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
  206. package/dist/enterprise/training/training-student.controller.js +22 -0
  207. package/dist/enterprise/training/training-student.controller.js.map +1 -1
  208. package/dist/enterprise/training/training-student.mcp-tools.d.ts +27 -0
  209. package/dist/enterprise/training/training-student.mcp-tools.d.ts.map +1 -0
  210. package/dist/enterprise/training/training-student.mcp-tools.js +186 -0
  211. package/dist/enterprise/training/training-student.mcp-tools.js.map +1 -0
  212. package/dist/enterprise/training/training-student.service.d.ts +32 -0
  213. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  214. package/dist/enterprise/training/training-student.service.js +138 -0
  215. package/dist/enterprise/training/training-student.service.js.map +1 -1
  216. package/dist/evaluation/evaluation.mcp-tools.d.ts +25 -0
  217. package/dist/evaluation/evaluation.mcp-tools.d.ts.map +1 -0
  218. package/dist/evaluation/evaluation.mcp-tools.js +220 -0
  219. package/dist/evaluation/evaluation.mcp-tools.js.map +1 -0
  220. package/dist/evaluation/evaluation.module.d.ts.map +1 -1
  221. package/dist/evaluation/evaluation.module.js +2 -1
  222. package/dist/evaluation/evaluation.module.js.map +1 -1
  223. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  224. package/dist/evaluation/evaluation.service.js +9 -2
  225. package/dist/evaluation/evaluation.service.js.map +1 -1
  226. package/dist/exam/dto/create-exam-question.dto.d.ts +2 -0
  227. package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -1
  228. package/dist/exam/dto/create-exam-question.dto.js +10 -0
  229. package/dist/exam/dto/create-exam-question.dto.js.map +1 -1
  230. package/dist/exam/dto/create-exam.dto.d.ts +2 -0
  231. package/dist/exam/dto/create-exam.dto.d.ts.map +1 -1
  232. package/dist/exam/dto/create-exam.dto.js +10 -0
  233. package/dist/exam/dto/create-exam.dto.js.map +1 -1
  234. package/dist/exam/dto/create-question-subject.dto.d.ts +5 -0
  235. package/dist/exam/dto/create-question-subject.dto.d.ts.map +1 -0
  236. package/dist/exam/dto/create-question-subject.dto.js +28 -0
  237. package/dist/exam/dto/create-question-subject.dto.js.map +1 -0
  238. package/dist/exam/exam-attempt.controller.d.ts +4 -0
  239. package/dist/exam/exam-attempt.controller.d.ts.map +1 -1
  240. package/dist/exam/exam-attempt.service.d.ts +7 -1
  241. package/dist/exam/exam-attempt.service.d.ts.map +1 -1
  242. package/dist/exam/exam-attempt.service.js +47 -17
  243. package/dist/exam/exam-attempt.service.js.map +1 -1
  244. package/dist/exam/exam.controller.d.ts +34 -0
  245. package/dist/exam/exam.controller.d.ts.map +1 -1
  246. package/dist/exam/exam.controller.js +27 -0
  247. package/dist/exam/exam.controller.js.map +1 -1
  248. package/dist/exam/exam.mcp-tools.d.ts +62 -0
  249. package/dist/exam/exam.mcp-tools.d.ts.map +1 -0
  250. package/dist/exam/exam.mcp-tools.js +430 -0
  251. package/dist/exam/exam.mcp-tools.js.map +1 -0
  252. package/dist/exam/exam.module.d.ts.map +1 -1
  253. package/dist/exam/exam.module.js +2 -1
  254. package/dist/exam/exam.module.js.map +1 -1
  255. package/dist/exam/exam.service.d.ts +38 -0
  256. package/dist/exam/exam.service.d.ts.map +1 -1
  257. package/dist/exam/exam.service.js +114 -17
  258. package/dist/exam/exam.service.js.map +1 -1
  259. package/dist/index.d.ts +10 -0
  260. package/dist/index.d.ts.map +1 -1
  261. package/dist/index.js +10 -0
  262. package/dist/index.js.map +1 -1
  263. package/dist/instructor/instructor.mcp-tools.d.ts +41 -0
  264. package/dist/instructor/instructor.mcp-tools.d.ts.map +1 -0
  265. package/dist/instructor/instructor.mcp-tools.js +326 -0
  266. package/dist/instructor/instructor.mcp-tools.js.map +1 -0
  267. package/dist/instructor/instructor.module.d.ts.map +1 -1
  268. package/dist/instructor/instructor.module.js +2 -1
  269. package/dist/instructor/instructor.module.js.map +1 -1
  270. package/dist/lms.module.d.ts.map +1 -1
  271. package/dist/lms.module.js +18 -0
  272. package/dist/lms.module.js.map +1 -1
  273. package/dist/realtime/lms-realtime.controller.d.ts +7 -0
  274. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -0
  275. package/dist/realtime/lms-realtime.controller.js +34 -0
  276. package/dist/realtime/lms-realtime.controller.js.map +1 -0
  277. package/dist/realtime/lms-realtime.module.d.ts +3 -0
  278. package/dist/realtime/lms-realtime.module.d.ts.map +1 -0
  279. package/dist/realtime/lms-realtime.module.js +25 -0
  280. package/dist/realtime/lms-realtime.module.js.map +1 -0
  281. package/dist/realtime/lms-realtime.service.d.ts +36 -0
  282. package/dist/realtime/lms-realtime.service.d.ts.map +1 -0
  283. package/dist/realtime/lms-realtime.service.js +59 -0
  284. package/dist/realtime/lms-realtime.service.js.map +1 -0
  285. package/dist/realtime/lms-realtime.subscriber.d.ts +10 -0
  286. package/dist/realtime/lms-realtime.subscriber.d.ts.map +1 -0
  287. package/dist/realtime/lms-realtime.subscriber.js +70 -0
  288. package/dist/realtime/lms-realtime.subscriber.js.map +1 -0
  289. package/dist/reports/reports.mcp-tools.d.ts +10 -0
  290. package/dist/reports/reports.mcp-tools.d.ts.map +1 -0
  291. package/dist/reports/reports.mcp-tools.js +50 -0
  292. package/dist/reports/reports.mcp-tools.js.map +1 -0
  293. package/dist/reports/reports.module.d.ts.map +1 -1
  294. package/dist/reports/reports.module.js +2 -1
  295. package/dist/reports/reports.module.js.map +1 -1
  296. package/dist/training/training.mcp-tools.d.ts +20 -0
  297. package/dist/training/training.mcp-tools.d.ts.map +1 -0
  298. package/dist/training/training.mcp-tools.js +181 -0
  299. package/dist/training/training.mcp-tools.js.map +1 -0
  300. package/dist/training/training.module.d.ts.map +1 -1
  301. package/dist/training/training.module.js +2 -1
  302. package/dist/training/training.module.js.map +1 -1
  303. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
  304. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
  305. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
  306. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
  307. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
  308. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
  309. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
  310. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
  311. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
  312. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
  313. package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
  314. package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
  315. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
  316. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
  317. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
  318. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
  319. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
  320. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
  321. package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
  322. package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
  323. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
  324. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
  325. package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
  326. package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
  327. package/hedhog/data/integration_event_catalog.yaml +69 -0
  328. package/hedhog/data/menu.yaml +51 -0
  329. package/hedhog/data/route.yaml +2484 -0
  330. package/hedhog/data/video_resolution_profile.yaml +7 -0
  331. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +432 -422
  332. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +200 -67
  333. package/hedhog/frontend/app/_components/course-picker.tsx.ejs +228 -0
  334. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
  335. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
  336. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
  337. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
  338. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
  339. package/hedhog/frontend/app/_lib/hooks/use-lms-realtime-refresh.ts.ejs +58 -0
  340. package/hedhog/frontend/app/achievements/page.tsx.ejs +850 -0
  341. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +1016 -0
  342. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +68 -5
  343. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
  344. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
  345. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
  346. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
  347. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +16 -5
  348. package/hedhog/frontend/app/classes/page.tsx.ejs +126 -2105
  349. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
  350. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
  351. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
  352. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
  353. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +51 -11
  354. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
  355. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
  356. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +2 -0
  357. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -96
  358. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +80 -66
  359. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1286 -230
  360. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1334 -153
  361. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
  362. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -1
  363. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
  364. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +106 -4
  365. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -1
  366. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +30 -7
  367. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +138 -6
  368. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +16 -2
  369. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
  370. package/hedhog/frontend/app/courses/page.tsx.ejs +282 -113
  371. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
  372. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
  373. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -0
  374. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +7 -0
  375. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
  376. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
  377. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
  378. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
  379. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
  380. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
  381. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +186 -5
  382. package/hedhog/frontend/app/exams/page.tsx.ejs +89 -26
  383. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +10 -3
  384. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
  385. package/hedhog/frontend/app/instructors/page.tsx.ejs +1 -0
  386. package/hedhog/frontend/app/layout.tsx.ejs +5 -1
  387. package/hedhog/frontend/app/paths/page.tsx.ejs +19 -29
  388. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
  389. package/hedhog/frontend/app/training/page.tsx.ejs +19 -29
  390. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
  391. package/hedhog/frontend/messages/en.json +563 -20
  392. package/hedhog/frontend/messages/pt.json +563 -20
  393. package/hedhog/query/triggers.sql +53 -0
  394. package/hedhog/table/achievement.yaml +46 -0
  395. package/hedhog/table/bitcode_wallet.yaml +18 -0
  396. package/hedhog/table/bitcode_wallet_transaction.yaml +22 -0
  397. package/hedhog/table/certificate.yaml +3 -0
  398. package/hedhog/table/course.yaml +8 -0
  399. package/hedhog/table/course_file.yaml +23 -0
  400. package/hedhog/table/course_lesson.yaml +5 -0
  401. package/hedhog/table/course_lesson_discussion_like.yaml +21 -0
  402. package/hedhog/table/course_lesson_discussion_topic.yaml +35 -0
  403. package/hedhog/table/course_lesson_file.yaml +8 -0
  404. package/hedhog/table/course_lesson_note.yaml +34 -0
  405. package/hedhog/table/course_video_resolution_profile.yaml +22 -0
  406. package/hedhog/table/exam.yaml +5 -0
  407. package/hedhog/table/learning_path_enrollment.yaml +6 -0
  408. package/hedhog/table/question.yaml +10 -0
  409. package/hedhog/table/question_subject.yaml +17 -0
  410. package/hedhog/table/student_activity_streak.yaml +25 -0
  411. package/hedhog/table/video_resolution_profile.yaml +18 -0
  412. package/package.json +8 -7
  413. package/src/achievement/achievement.controller.ts +60 -0
  414. package/src/achievement/achievement.mcp-tools.ts +108 -0
  415. package/src/achievement/achievement.module.ts +13 -0
  416. package/src/achievement/achievement.service.ts +252 -0
  417. package/src/achievement/dto/create-achievement.dto.ts +50 -0
  418. package/src/achievement/dto/update-achievement.dto.ts +47 -0
  419. package/src/bitcode-wallet/bitcode-wallet.controller.ts +69 -0
  420. package/src/bitcode-wallet/bitcode-wallet.mcp-tools.ts +107 -0
  421. package/src/bitcode-wallet/bitcode-wallet.module.ts +13 -0
  422. package/src/bitcode-wallet/bitcode-wallet.service.ts +361 -0
  423. package/src/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.ts +27 -0
  424. package/src/bitcode-wallet/dto/create-bitcode-wallet.dto.ts +7 -0
  425. package/src/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.ts +28 -0
  426. package/src/bitcode-wallet/dto/update-bitcode-wallet.dto.ts +8 -0
  427. package/src/certificate/certificate.controller.ts +17 -3
  428. package/src/certificate/certificate.mcp-tools.ts +131 -0
  429. package/src/certificate/certificate.module.ts +2 -1
  430. package/src/certificate/certificate.service.ts +193 -7
  431. package/src/certificate/dto/update-certificate-public-access.dto.ts +6 -0
  432. package/src/class-group/class-group.mcp-tools.ts +435 -0
  433. package/src/class-group/class-group.module.ts +2 -1
  434. package/src/class-group/class-group.service.ts +51 -1
  435. package/src/course/course-operations-integration.service.ts +520 -0
  436. package/src/course/course-structure.controller.ts +46 -10
  437. package/src/course/course-structure.service.ts +236 -27
  438. package/src/course/course-video-conversion.service.ts +415 -0
  439. package/src/course/course.controller.ts +18 -0
  440. package/src/course/course.mcp-tools.ts +409 -0
  441. package/src/course/course.module.ts +23 -3
  442. package/src/course/course.service.ts +178 -29
  443. package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
  444. package/src/course/dto/create-course.dto.ts +16 -0
  445. package/src/course/dto/update-course-resources.dto.ts +39 -0
  446. package/src/course-lesson-discussion/course-lesson-discussion.controller.ts +55 -0
  447. package/src/course-lesson-discussion/course-lesson-discussion.mcp-tools.ts +75 -0
  448. package/src/course-lesson-discussion/course-lesson-discussion.module.ts +13 -0
  449. package/src/course-lesson-discussion/course-lesson-discussion.service.ts +354 -0
  450. package/src/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.ts +16 -0
  451. package/src/course-lesson-note/course-lesson-note.controller.ts +68 -0
  452. package/src/course-lesson-note/course-lesson-note.mcp-tools.ts +96 -0
  453. package/src/course-lesson-note/course-lesson-note.module.ts +13 -0
  454. package/src/course-lesson-note/course-lesson-note.service.ts +248 -0
  455. package/src/course-lesson-note/dto/create-course-lesson-note.dto.ts +16 -0
  456. package/src/course-lesson-note/dto/update-course-lesson-note.dto.ts +18 -0
  457. package/src/dashboard/dashboard.mcp-tools.ts +23 -0
  458. package/src/dashboard/dashboard.module.ts +2 -1
  459. package/src/enterprise/enterprise.controller.ts +0 -1
  460. package/src/enterprise/enterprise.mcp-tools.ts +403 -0
  461. package/src/enterprise/enterprise.module.ts +2 -1
  462. package/src/enterprise/training/enterprise-training.module.ts +11 -1
  463. package/src/enterprise/training/training-admin.mcp-tools.ts +479 -0
  464. package/src/enterprise/training/training-instructor.mcp-tools.ts +210 -0
  465. package/src/enterprise/training/training-student.controller.ts +17 -1
  466. package/src/enterprise/training/training-student.mcp-tools.ts +136 -0
  467. package/src/enterprise/training/training-student.service.ts +167 -1
  468. package/src/evaluation/evaluation.mcp-tools.ts +155 -0
  469. package/src/evaluation/evaluation.module.ts +2 -1
  470. package/src/evaluation/evaluation.service.ts +9 -2
  471. package/src/exam/dto/create-exam-question.dto.ts +8 -0
  472. package/src/exam/dto/create-exam.dto.ts +8 -0
  473. package/src/exam/dto/create-question-subject.dto.ts +12 -0
  474. package/src/exam/exam-attempt.service.ts +46 -14
  475. package/src/exam/exam.controller.ts +19 -0
  476. package/src/exam/exam.mcp-tools.ts +337 -0
  477. package/src/exam/exam.module.ts +2 -1
  478. package/src/exam/exam.service.ts +121 -0
  479. package/src/index.ts +10 -0
  480. package/src/instructor/instructor.mcp-tools.ts +243 -0
  481. package/src/instructor/instructor.module.ts +2 -1
  482. package/src/lms.module.ts +18 -1
  483. package/src/realtime/lms-realtime.controller.ts +12 -0
  484. package/src/realtime/lms-realtime.module.ts +12 -0
  485. package/src/realtime/lms-realtime.service.ts +98 -0
  486. package/src/realtime/lms-realtime.subscriber.ts +61 -0
  487. package/src/reports/reports.mcp-tools.ts +27 -0
  488. package/src/reports/reports.module.ts +2 -1
  489. package/src/training/training.mcp-tools.ts +128 -0
  490. package/src/training/training.module.ts +2 -1
  491. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
  492. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
  493. package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
  494. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
  495. package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
  496. package/src/video-resolution-profile/video-resolution-profile.service.ts +117 -0
@@ -9,21 +9,29 @@ import {
9
9
  CheckCircle2,
10
10
  CircleDot,
11
11
  Clock,
12
+ Download,
13
+ File as FileIcon,
14
+ FileImage,
15
+ FileText,
12
16
  Layers,
13
17
  Loader2,
18
+ Pencil,
14
19
  Plus,
15
20
  Save,
16
21
  Undo2,
22
+ UploadCloud,
17
23
  Video,
24
+ X,
18
25
  } from 'lucide-react';
19
26
  import { useTranslations } from 'next-intl';
20
27
  import { useRouter } from 'next/navigation';
21
- import { type ChangeEvent, useEffect, useMemo, useState } from 'react';
28
+ import { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
22
29
  import { useForm } from 'react-hook-form';
23
30
  import { toast } from 'sonner';
24
31
  import { z } from 'zod';
25
32
 
26
33
  import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
34
+ import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
27
35
  import { RichTextEditor } from '@/components/rich-text-editor';
28
36
  import { Badge } from '@/components/ui/badge';
29
37
  import { Button } from '@/components/ui/button';
@@ -36,6 +44,7 @@ import {
36
44
  DialogHeader,
37
45
  DialogTitle,
38
46
  } from '@/components/ui/dialog';
47
+ import { EntityPicker } from '@/components/ui/entity-picker';
39
48
  import {
40
49
  Form,
41
50
  FormControl,
@@ -45,17 +54,25 @@ import {
45
54
  FormMessage,
46
55
  } from '@/components/ui/form';
47
56
  import { Input } from '@/components/ui/input';
48
- import { ScrollArea } from '@/components/ui/scroll-area';
49
57
  import { Separator } from '@/components/ui/separator';
50
58
  import {
51
59
  Sheet,
52
- SheetContent,
53
60
  SheetDescription,
54
61
  SheetFooter,
55
62
  SheetHeader,
56
63
  SheetTitle,
57
64
  } from '@/components/ui/sheet';
65
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
66
+ import { Skeleton } from '@/components/ui/skeleton';
58
67
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
68
+ import {
69
+ Tooltip,
70
+ TooltipContent,
71
+ TooltipProvider,
72
+ TooltipTrigger,
73
+ } from '@/components/ui/tooltip';
74
+ import { useIsMobile } from '@/hooks/use-mobile';
75
+ import { cn } from '@/lib/utils';
59
76
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
60
77
  import { useMutation, useQueryClient } from '@tanstack/react-query';
61
78
 
@@ -71,9 +88,16 @@ import { CourseDangerZoneCard } from '../../_components/CourseDangerZoneCard';
71
88
  import { CourseFlagsCard } from '../../_components/CourseFlagsCard';
72
89
  import { CourseMediaCard } from '../../_components/CourseMediaCard';
73
90
  import { CourseRelationsCard } from '../../_components/CourseRelationsCard';
91
+ import {
92
+ deleteFile,
93
+ getCourseResources,
94
+ updateCourseResources,
95
+ uploadFile,
96
+ } from '../_data/services/course-structure.service';
74
97
  import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
75
98
  import { courseStructureQueryKey } from '../_data/use-course-structure-query';
76
99
  import { useStructureStore } from './store';
100
+ import { Resource } from './types';
77
101
 
78
102
  // ── API types (local to this component) ──────────────────────────────────────
79
103
 
@@ -81,13 +105,14 @@ type ApiCourseDetail = {
81
105
  id: number;
82
106
  name: string;
83
107
  slug: string;
108
+ code?: string | null;
84
109
  title: string;
85
110
  description: string;
86
111
  primaryColor?: string | null;
87
112
  secondaryColor?: string | null;
88
113
  level: 'beginner' | 'intermediate' | 'advanced';
89
114
  status: 'draft' | 'published' | 'archived';
90
- offeringType: 'scheduled' | 'on_demand' | 'blended';
115
+ offeringType?: 'scheduled' | 'on_demand' | 'blended' | null;
91
116
  categories: string[];
92
117
  isFeatured: boolean;
93
118
  hasCertificate: boolean;
@@ -119,6 +144,16 @@ type ApiCourseDetail = {
119
144
  aspectRatio: string | null;
120
145
  } | null;
121
146
  certificateModel?: string | null;
147
+ operationsProjectId?: number | null;
148
+ operationsProjectCode?: string | null;
149
+ operationsProjectName?: string | null;
150
+ };
151
+
152
+ type VideoProfileOption = {
153
+ id: number;
154
+ name: string;
155
+ ffmpeg_params: string;
156
+ status: string;
122
157
  };
123
158
 
124
159
  type ApiCategory = { id: number; slug: string; name: string };
@@ -165,6 +200,27 @@ type ApiCertificateTemplateList = {
165
200
  pageSize: number;
166
201
  lastPage?: number;
167
202
  };
203
+ type ApiOperationsProjectOption = {
204
+ id: number;
205
+ code?: string | null;
206
+ name: string;
207
+ label?: string;
208
+ clientName?: string | null;
209
+ status?: string;
210
+ };
211
+
212
+ type CourseResourceApi = {
213
+ id: number;
214
+ nome: string;
215
+ fileId?: number | null;
216
+ tipo?: string | null;
217
+ publico?: boolean;
218
+ };
219
+
220
+ type CourseResourceItem = Resource & {
221
+ fileId?: number | null;
222
+ };
223
+
168
224
  type Locale = { id?: number; code: string; name: string };
169
225
 
170
226
  // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -208,12 +264,23 @@ function toApiStatus(status: CourseEditFormValues['status']) {
208
264
  }
209
265
 
210
266
  function toPtOfferingType(
211
- value: ApiCourseDetail['offeringType']
267
+ value?: ApiCourseDetail['offeringType'] | string | null
212
268
  ): CourseEditFormValues['tipoOferta'] {
213
269
  const n = normalizeEnumValue(value);
214
270
  if (n === 'scheduled' || n === 'agendado') return 'agendado';
215
271
  if (n === 'blended' || n === 'hibrido') return 'hibrido';
216
- return 'sob_demanda';
272
+ if (
273
+ n === 'on_demand' ||
274
+ n === 'ondemand' ||
275
+ n === 'on-demand' ||
276
+ n === 'sob_demanda' ||
277
+ n === 'sob demanda'
278
+ ) {
279
+ return 'sob_demanda';
280
+ }
281
+
282
+ // Safe fallback: keep video profiles tab hidden when backend sends invalid data.
283
+ return 'agendado';
217
284
  }
218
285
 
219
286
  function toApiOfferingType(value: CourseEditFormValues['tipoOferta']) {
@@ -248,6 +315,37 @@ function getInstructorAvatarUrl(avatarId?: number | null) {
248
315
  : null;
249
316
  }
250
317
 
318
+ function getResourceIcon(type: string): typeof FileIcon {
319
+ if (type === 'application/pdf' || type.endsWith('pdf')) return FileText;
320
+ if (type.startsWith('image/')) return FileImage;
321
+ return FileIcon;
322
+ }
323
+
324
+ function getResourceIconColor(type: string): string {
325
+ if (type === 'application/pdf' || type.endsWith('pdf')) return 'text-red-500';
326
+ if (type.startsWith('image/')) return 'text-blue-500';
327
+ return 'text-muted-foreground';
328
+ }
329
+
330
+ function formatFileSize(bytes: number): string {
331
+ if (bytes < 1024) return `${bytes} B`;
332
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
333
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
334
+ }
335
+
336
+ function mapApiCourseResourceToLocal(
337
+ item: CourseResourceApi
338
+ ): CourseResourceItem {
339
+ return {
340
+ id: String(item.id),
341
+ fileId: item.fileId ?? undefined,
342
+ name: item.nome,
343
+ size: '',
344
+ type: item.tipo ?? 'file',
345
+ public: item.publico ?? true,
346
+ };
347
+ }
348
+
251
349
  // ── Schema ────────────────────────────────────────────────────────────────────
252
350
 
253
351
  function buildSchema(t: (key: string) => string) {
@@ -257,10 +355,12 @@ function buildSchema(t: (key: string) => string) {
257
355
  .trim()
258
356
  .min(2, t('validation.codeMin'))
259
357
  .max(32, t('validation.codeMax'))
260
- .regex(
261
- /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
262
- 'Código: apenas letras minúsculas, números e hífens'
263
- ),
358
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, t('validation.codeFormat')),
359
+ code: z
360
+ .string()
361
+ .regex(/^[A-Z0-9]{2,}$/, t('validation.editionCodeFormat'))
362
+ .or(z.literal(''))
363
+ .optional(),
264
364
  nomeInterno: z.string().trim().min(3, t('validation.internalNameMin')),
265
365
  tituloComercial: z.string().trim().min(3, t('validation.titleMin')),
266
366
  descricaoPublica: z.string().trim(),
@@ -270,6 +370,7 @@ function buildSchema(t: (key: string) => string) {
270
370
  status: z.enum(['ativo', 'rascunho', 'arquivado']),
271
371
  tipoOferta: z.enum(['agendado', 'sob_demanda', 'hibrido']),
272
372
  categorias: z.array(z.string()).optional().default([]),
373
+ operationsProjectId: z.string().optional(),
273
374
  primaryColor: z
274
375
  .string()
275
376
  .regex(/^#([0-9A-Fa-f]{6})$/, t('validation.colorInvalid')),
@@ -297,6 +398,7 @@ export function EditorCourse() {
297
398
  const { request, currentLocaleCode, locales } = useApp();
298
399
  const t = useTranslations('lms.CursoEditPage');
299
400
  const router = useRouter();
401
+ const isMobile = useIsMobile();
300
402
  const queryClient = useQueryClient();
301
403
  const createSessionMutation = useCreateSessionMutation();
302
404
 
@@ -337,6 +439,25 @@ export function EditorCourse() {
337
439
  >([]);
338
440
  const [persistedCertificateModel, setPersistedCertificateModel] =
339
441
  useState('');
442
+ const [courseResources, setCourseResources] = useState<CourseResourceItem[]>(
443
+ []
444
+ );
445
+ const [resourcesDragOver, setResourcesDragOver] = useState(false);
446
+ const [isUploadingResources, setIsUploadingResources] = useState(false);
447
+ const [isSavingResources, setIsSavingResources] = useState(false);
448
+ const resourcesInputRef = useRef<HTMLInputElement>(null);
449
+ const [linkedProfileIds, setLinkedProfileIds] = useState<number[]>([]);
450
+ const [videoProfilePickerResetKey, setVideoProfilePickerResetKey] =
451
+ useState(0);
452
+ const [videoProfileEditSheetOpen, setVideoProfileEditSheetOpen] =
453
+ useState(false);
454
+ const [editingVideoProfileId, setEditingVideoProfileId] = useState<
455
+ number | null
456
+ >(null);
457
+ const [editingVideoProfileName, setEditingVideoProfileName] = useState('');
458
+ const [editingVideoProfileParams, setEditingVideoProfileParams] =
459
+ useState('');
460
+ const [savingVideoProfileEdit, setSavingVideoProfileEdit] = useState(false);
340
461
 
341
462
  // ── Queries ─────────────────────────────────────────────────────────────────
342
463
  const { data: apiCourse, refetch: refetchCourse } = useQuery<ApiCourseDetail>(
@@ -412,7 +533,7 @@ export function EditorCourse() {
412
533
  >({
413
534
  url: '/lms/certificates/templates',
414
535
  method: 'GET',
415
- params: { page: 1, pageSize: 100 },
536
+ params: { page: 1, pageSize: 100, status: 'all' },
416
537
  });
417
538
  const payload = response.data;
418
539
  if (Array.isArray(payload))
@@ -428,6 +549,42 @@ export function EditorCourse() {
428
549
  initialData: { data: [], total: 0, page: 1, pageSize: 100, lastPage: 1 },
429
550
  });
430
551
 
552
+ const { data: operationsProjectOptionsData } = useQuery<
553
+ { data: ApiOperationsProjectOption[] } | ApiOperationsProjectOption[]
554
+ >({
555
+ queryKey: ['lms-course-operations-project-options'],
556
+ queryFn: async () => {
557
+ try {
558
+ const response = await request<
559
+ { data: ApiOperationsProjectOption[] } | ApiOperationsProjectOption[]
560
+ >({
561
+ url: '/operations/projects/options?pageSize=200&sortField=name&sortOrder=asc',
562
+ method: 'GET',
563
+ });
564
+ return response.data;
565
+ } catch {
566
+ return { data: [] };
567
+ }
568
+ },
569
+ initialData: { data: [] },
570
+ });
571
+
572
+ const {
573
+ data: allVideoProfiles = [],
574
+ refetch: refetchVideoProfiles,
575
+ isFetching: isFetchingVideoProfiles,
576
+ } = useQuery<VideoProfileOption[]>({
577
+ queryKey: ['lms-video-resolution-profiles-all'],
578
+ queryFn: async () => {
579
+ const response = await request<VideoProfileOption[]>({
580
+ url: '/lms/video-resolution-profiles/all',
581
+ method: 'GET',
582
+ });
583
+ return response.data;
584
+ },
585
+ initialData: [],
586
+ });
587
+
431
588
  // ── Save mutation ───────────────────────────────────────────────────────────
432
589
  const { mutate: saveCourse, isPending: saving } = useMutation({
433
590
  mutationFn: async (data: CourseEditFormValues) => {
@@ -437,6 +594,7 @@ export function EditorCourse() {
437
594
  data: {
438
595
  name: data.nomeInterno.trim(),
439
596
  slug: data.slug.trim().toLowerCase(),
597
+ code: data.code?.trim() || undefined,
440
598
  title: data.tituloComercial.trim(),
441
599
  description: data.descricaoPublica.trim(),
442
600
  requirements: data.preRequisitos?.trim() || undefined,
@@ -455,11 +613,23 @@ export function EditorCourse() {
455
613
  isListed: data.listado,
456
614
  certificateModel: data.modeloCertificado.trim() || null,
457
615
  instructorIds: (data.instrutores ?? []).map(Number),
616
+ operationsProjectId: data.operationsProjectId
617
+ ? Number(data.operationsProjectId)
618
+ : null,
458
619
  },
459
620
  });
460
621
  return data;
461
622
  },
462
623
  onSuccess: (data) => {
624
+ if (courseId) {
625
+ void request({
626
+ url: `/lms/courses/${courseId}/video-resolution-profiles/sync`,
627
+ method: 'POST',
628
+ data: { profileIds: linkedProfileIds },
629
+ }).catch(() => {
630
+ toast.error('Erro ao sincronizar perfis de vídeo.');
631
+ });
632
+ }
463
633
  setPersistedCertificateModel(data.modeloCertificado || '');
464
634
  updateCourseInStore({
465
635
  title: data.tituloComercial,
@@ -486,6 +656,7 @@ export function EditorCourse() {
486
656
  resolver: zodResolver(schema),
487
657
  defaultValues: {
488
658
  slug: '',
659
+ code: '',
489
660
  nomeInterno: '',
490
661
  tituloComercial: '',
491
662
  descricaoPublica: '',
@@ -495,6 +666,7 @@ export function EditorCourse() {
495
666
  status: 'rascunho',
496
667
  tipoOferta: 'sob_demanda',
497
668
  categorias: [],
669
+ operationsProjectId: '',
498
670
  primaryColor: '#1D4ED8',
499
671
  secondaryColor: '#111827',
500
672
  instrutores: [],
@@ -556,6 +728,35 @@ export function EditorCourse() {
556
728
  );
557
729
  }, [categoryListData, createdCategoryOptions]);
558
730
 
731
+ const projectOptions = useMemo<PickerOption[]>(() => {
732
+ const payload = Array.isArray(operationsProjectOptionsData)
733
+ ? operationsProjectOptionsData
734
+ : (operationsProjectOptionsData?.data ?? []);
735
+
736
+ const currentOption =
737
+ apiCourse?.operationsProjectId && apiCourse?.operationsProjectName
738
+ ? [
739
+ {
740
+ value: String(apiCourse.operationsProjectId),
741
+ label: apiCourse.operationsProjectName,
742
+ description: apiCourse.operationsProjectCode ?? null,
743
+ },
744
+ ]
745
+ : [];
746
+
747
+ const remoteOptions = payload.map((project) => ({
748
+ value: String(project.id),
749
+ label: project.name,
750
+ description: project.code ?? project.clientName ?? null,
751
+ meta: project.status ?? null,
752
+ }));
753
+
754
+ return [...currentOption, ...remoteOptions].filter(
755
+ (item, index, list) =>
756
+ list.findIndex((candidate) => candidate.value === item.value) === index
757
+ );
758
+ }, [apiCourse, operationsProjectOptionsData]);
759
+
559
760
  const instructorOptions = useMemo(() => {
560
761
  const fromCourse = (apiCourse?.instructors ?? []).map((item) => ({
561
762
  value: String(item.id),
@@ -586,14 +787,84 @@ export function EditorCourse() {
586
787
  );
587
788
  }, [certificateTemplateData, createdTemplateOptions]);
588
789
 
790
+ const availableVideoProfiles = useMemo(
791
+ () =>
792
+ allVideoProfiles.filter(
793
+ (profile) => !linkedProfileIds.includes(profile.id)
794
+ ),
795
+ [allVideoProfiles, linkedProfileIds]
796
+ );
797
+
589
798
  // ── Structural stats ─────────────────────────────────────────────────────────
590
799
  const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
591
800
  const hours = Math.floor(totalMinutes / 60);
592
801
  const minutes = totalMinutes % 60;
802
+ const durationLabel = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
593
803
  const publishedCount = lessons.filter(
594
804
  (l) => l.visibility === 'publico' || l.status === 'publicada'
595
805
  ).length;
596
806
 
807
+ const formValues = form.watch();
808
+ const showVideoTab =
809
+ formValues.tipoOferta === 'sob_demanda' ||
810
+ formValues.tipoOferta === 'hibrido';
811
+ const descriptionText = String(formValues.descricaoPublica ?? '')
812
+ .replace(/<[^>]+>/g, '')
813
+ .trim();
814
+ const checklistItems = [
815
+ {
816
+ id: 'title',
817
+ label: t('structureEditor.publishChecklist.items.title'),
818
+ done: formValues.tituloComercial.trim().length >= 3,
819
+ required: true,
820
+ },
821
+ {
822
+ id: 'description',
823
+ label: t('structureEditor.publishChecklist.items.description'),
824
+ done: descriptionText.length > 0,
825
+ required: true,
826
+ },
827
+ {
828
+ id: 'media',
829
+ label: t('structureEditor.publishChecklist.items.media'),
830
+ done: Boolean(apiCourse?.logoFileId || apiCourse?.bannerFileId),
831
+ required: true,
832
+ },
833
+ {
834
+ id: 'session',
835
+ label: t('structureEditor.publishChecklist.items.session'),
836
+ done: sessions.length > 0,
837
+ required: true,
838
+ },
839
+ {
840
+ id: 'lesson',
841
+ label: t('structureEditor.publishChecklist.items.lesson'),
842
+ done: lessons.length > 0,
843
+ required: true,
844
+ },
845
+ {
846
+ id: 'published-lessons',
847
+ label: t('structureEditor.publishChecklist.items.publishedLessons'),
848
+ done: publishedCount > 0,
849
+ required: true,
850
+ },
851
+ {
852
+ id: 'certificate',
853
+ label: t('structureEditor.publishChecklist.items.certificate'),
854
+ done:
855
+ !formValues.certificado ||
856
+ String(formValues.modeloCertificado ?? '').trim().length > 0,
857
+ required: formValues.certificado,
858
+ },
859
+ ];
860
+ const requiredChecklist = checklistItems.filter((item) => item.required);
861
+ const completedRequired = requiredChecklist.filter(
862
+ (item) => item.done
863
+ ).length;
864
+ const isReadyToPublish =
865
+ requiredChecklist.length > 0 &&
866
+ completedRequired === requiredChecklist.length;
867
+
597
868
  // ── Effects ──────────────────────────────────────────────────────────────────
598
869
  useEffect(() => {
599
870
  if (!apiCourse) return;
@@ -601,6 +872,7 @@ export function EditorCourse() {
601
872
  apiCourse.certificateModel ?? persistedCertificateModel ?? '';
602
873
  form.reset({
603
874
  slug: apiCourse.slug,
875
+ code: apiCourse.code ?? '',
604
876
  nomeInterno: apiCourse.name,
605
877
  tituloComercial: apiCourse.title,
606
878
  descricaoPublica: apiCourse.description ?? '',
@@ -610,6 +882,9 @@ export function EditorCourse() {
610
882
  status: toPtStatus(apiCourse.status),
611
883
  tipoOferta: toPtOfferingType(apiCourse.offeringType),
612
884
  categorias: apiCourse.categories ?? [],
885
+ operationsProjectId: apiCourse.operationsProjectId
886
+ ? String(apiCourse.operationsProjectId)
887
+ : '',
613
888
  primaryColor: apiCourse.primaryColor || '#1D4ED8',
614
889
  secondaryColor: apiCourse.secondaryColor || '#111827',
615
890
  instrutores: (apiCourse.instructorIds ?? []).map(String),
@@ -643,6 +918,164 @@ export function EditorCourse() {
643
918
  });
644
919
  }, [apiCourse?.bannerFileId, apiCourse?.logoFileId, request]);
645
920
 
921
+ useEffect(() => {
922
+ if (!courseId) return;
923
+
924
+ let active = true;
925
+
926
+ void getCourseResources(request, courseId)
927
+ .then((items) => {
928
+ if (!active) return;
929
+ setCourseResources(items.map(mapApiCourseResourceToLocal));
930
+ })
931
+ .catch(() => {
932
+ if (!active) return;
933
+ setCourseResources([]);
934
+ toast.error('Nao foi possivel carregar os recursos do curso.');
935
+ });
936
+
937
+ return () => {
938
+ active = false;
939
+ };
940
+ }, [courseId, request]);
941
+
942
+ useEffect(() => {
943
+ if (!showVideoTab && activeTab === 'videos') {
944
+ setActiveTab('estrutura');
945
+ }
946
+ }, [showVideoTab, activeTab]);
947
+
948
+ useEffect(() => {
949
+ if (!courseId) return;
950
+ let active = true;
951
+ void request<VideoProfileOption[]>({
952
+ url: `/lms/courses/${courseId}/video-resolution-profiles`,
953
+ method: 'GET',
954
+ })
955
+ .then((res) => {
956
+ if (!active) return;
957
+ setLinkedProfileIds((res.data ?? []).map((p) => p.id));
958
+ })
959
+ .catch(() => {
960
+ if (!active) return;
961
+ setLinkedProfileIds([]);
962
+ });
963
+ return () => {
964
+ active = false;
965
+ };
966
+ }, [courseId, request]);
967
+
968
+ async function syncCourseResources(nextResources: CourseResourceItem[]) {
969
+ setIsSavingResources(true);
970
+ try {
971
+ const saved = await updateCourseResources(request, courseId, {
972
+ recursos: nextResources.map((item) => ({
973
+ nome: item.name,
974
+ ...(typeof item.fileId === 'number' && item.fileId > 0
975
+ ? { fileId: item.fileId }
976
+ : {}),
977
+ ...(item.type ? { tipo: item.type } : {}),
978
+ publico: item.public,
979
+ })),
980
+ });
981
+
982
+ setCourseResources(saved.map(mapApiCourseResourceToLocal));
983
+ toast.success('Recursos do curso atualizados.');
984
+ } catch {
985
+ toast.error('Nao foi possivel salvar os recursos do curso.');
986
+ void getCourseResources(request, courseId)
987
+ .then((items) =>
988
+ setCourseResources(items.map(mapApiCourseResourceToLocal))
989
+ )
990
+ .catch(() => undefined);
991
+ } finally {
992
+ setIsSavingResources(false);
993
+ }
994
+ }
995
+
996
+ async function handleCourseResourceFiles(files: File[]) {
997
+ if (files.length === 0) return;
998
+
999
+ setIsUploadingResources(true);
1000
+ try {
1001
+ const results = await Promise.allSettled(
1002
+ files.map((file) =>
1003
+ uploadFile(request, file, 'lms/courses/resources').then(
1004
+ (uploaded): CourseResourceItem => ({
1005
+ id: `new-${uploaded.id}`,
1006
+ fileId: uploaded.id,
1007
+ name: file.name,
1008
+ size: formatFileSize(file.size),
1009
+ type: file.type || file.name.split('.').pop() || 'file',
1010
+ public: true,
1011
+ })
1012
+ )
1013
+ )
1014
+ );
1015
+
1016
+ const succeeded = results
1017
+ .filter(
1018
+ (result): result is PromiseFulfilledResult<CourseResourceItem> =>
1019
+ result.status === 'fulfilled'
1020
+ )
1021
+ .map((result) => result.value);
1022
+
1023
+ const failedCount = results.filter(
1024
+ (result) => result.status === 'rejected'
1025
+ ).length;
1026
+
1027
+ if (failedCount > 0) {
1028
+ toast.error(`Falha ao enviar ${failedCount} arquivo(s).`);
1029
+ }
1030
+
1031
+ if (succeeded.length > 0) {
1032
+ const nextResources = [...courseResources, ...succeeded];
1033
+ await syncCourseResources(nextResources);
1034
+ }
1035
+ } finally {
1036
+ setIsUploadingResources(false);
1037
+ }
1038
+ }
1039
+
1040
+ async function handleRemoveCourseResource(item: CourseResourceItem) {
1041
+ const nextResources = courseResources.filter(
1042
+ (resource) => resource.id !== item.id
1043
+ );
1044
+ await syncCourseResources(nextResources);
1045
+
1046
+ if (item.fileId) {
1047
+ try {
1048
+ await deleteFile(request, item.fileId);
1049
+ } catch {
1050
+ toast.error(
1051
+ 'Recurso removido do curso, mas nao foi possivel apagar o arquivo fisico.'
1052
+ );
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ async function handleDownloadCourseResource(item: CourseResourceItem) {
1058
+ if (!item.fileId) {
1059
+ toast.error('Recurso sem arquivo para download.');
1060
+ return;
1061
+ }
1062
+
1063
+ try {
1064
+ const response = await request<{ url?: string }>({
1065
+ url: `/file/open/${item.fileId}`,
1066
+ method: 'PUT',
1067
+ });
1068
+ const url = response?.data?.url;
1069
+ if (!url) {
1070
+ toast.error('Nao foi possivel gerar o link de download.');
1071
+ return;
1072
+ }
1073
+ window.open(url, '_blank', 'noopener,noreferrer');
1074
+ } catch {
1075
+ toast.error('Nao foi possivel baixar o recurso.');
1076
+ }
1077
+ }
1078
+
646
1079
  // ── File handlers ────────────────────────────────────────────────────────────
647
1080
  async function openUploadedFile(fileId?: number | null) {
648
1081
  if (!fileId) return;
@@ -817,6 +1250,38 @@ export function EditorCourse() {
817
1250
  setInstructorEditSheetOpen(true);
818
1251
  }
819
1252
 
1253
+ function handleEditVideoProfile(profileId: number) {
1254
+ const profile = allVideoProfiles.find((p) => p.id === profileId);
1255
+ if (!profile) return;
1256
+ setEditingVideoProfileId(profileId);
1257
+ setEditingVideoProfileName(profile.name);
1258
+ setEditingVideoProfileParams(profile.ffmpeg_params);
1259
+ setVideoProfileEditSheetOpen(true);
1260
+ }
1261
+
1262
+ async function handleSaveVideoProfileEdit() {
1263
+ if (!editingVideoProfileId) return;
1264
+ try {
1265
+ setSavingVideoProfileEdit(true);
1266
+ await request({
1267
+ url: `/lms/video-resolution-profiles/${editingVideoProfileId}`,
1268
+ method: 'PATCH',
1269
+ data: {
1270
+ name: editingVideoProfileName,
1271
+ ffmpeg_params: editingVideoProfileParams,
1272
+ },
1273
+ });
1274
+ await refetchVideoProfiles();
1275
+ setVideoProfileEditSheetOpen(false);
1276
+ setEditingVideoProfileId(null);
1277
+ toast.success('Perfil de vídeo atualizado com sucesso.');
1278
+ } catch {
1279
+ toast.error('Erro ao atualizar o perfil de vídeo.');
1280
+ } finally {
1281
+ setSavingVideoProfileEdit(false);
1282
+ }
1283
+ }
1284
+
820
1285
  async function handleEditCategory(categorySlug: string) {
821
1286
  const category = (categoryListData?.data ?? []).find(
822
1287
  (item) => item.slug === categorySlug
@@ -922,6 +1387,33 @@ export function EditorCourse() {
922
1387
  }
923
1388
 
924
1389
  function onSubmit(data: CourseEditFormValues) {
1390
+ if (data.status === 'ativo') {
1391
+ const requiredProfileIds = new Set(linkedProfileIds);
1392
+ const invalidLesson = lessons.find((lesson) => {
1393
+ if (lesson.type !== 'video' || lesson.videoProvider !== 'file_storage') {
1394
+ return false;
1395
+ }
1396
+
1397
+ if (requiredProfileIds.size === 0) {
1398
+ return true;
1399
+ }
1400
+
1401
+ const resourceTypes = new Set(lesson.resources.map((res) => res.type));
1402
+ return [...requiredProfileIds].some(
1403
+ (profileId) => !resourceTypes.has(`video_profile:${profileId}`)
1404
+ );
1405
+ });
1406
+
1407
+ if (invalidLesson) {
1408
+ toast.error(
1409
+ linkedProfileIds.length === 0
1410
+ ? 'Configure ao menos um perfil de vídeo antes de publicar o curso.'
1411
+ : `A aula "${invalidLesson.title}" precisa de um vídeo para cada perfil antes da publicação.`
1412
+ );
1413
+ return;
1414
+ }
1415
+ }
1416
+
925
1417
  saveCourse(data);
926
1418
  }
927
1419
 
@@ -953,263 +1445,759 @@ export function EditorCourse() {
953
1445
  {course.slug}
954
1446
  </p>
955
1447
  </div>
956
- <Badge
957
- variant={course.published ? 'default' : 'secondary'}
958
- className="shrink-0 text-xs"
959
- >
960
- {course.published ? 'Publicado' : 'Rascunho'}
961
- </Badge>
1448
+ <div className="flex min-w-0 max-w-[56%] flex-wrap justify-end gap-1 sm:max-w-none sm:flex-nowrap sm:items-center sm:gap-1.5 sm:shrink-0">
1449
+ <TooltipProvider>
1450
+ <Tooltip>
1451
+ <TooltipTrigger asChild>
1452
+ <Badge
1453
+ variant="outline"
1454
+ className={cn(
1455
+ 'h-5 gap-1 font-normal bg-sky-500/15 text-sky-700 dark:text-sky-400 border-sky-500/30 cursor-help',
1456
+ isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
1457
+ )}
1458
+ >
1459
+ <Layers className="size-3" />
1460
+ {sessions.length}
1461
+ {!isMobile && (
1462
+ <> {sessions.length === 1 ? 'sessão' : 'sessões'}</>
1463
+ )}
1464
+ </Badge>
1465
+ </TooltipTrigger>
1466
+ <TooltipContent side="bottom">
1467
+ Total de sessões cadastradas neste curso.
1468
+ </TooltipContent>
1469
+ </Tooltip>
1470
+
1471
+ <Tooltip>
1472
+ <TooltipTrigger asChild>
1473
+ <Badge
1474
+ variant="outline"
1475
+ className={cn(
1476
+ 'h-5 gap-1 font-normal bg-violet-500/15 text-violet-700 dark:text-violet-400 border-violet-500/30 cursor-help',
1477
+ isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
1478
+ )}
1479
+ >
1480
+ <Video className="size-3" />
1481
+ {lessons.length}
1482
+ {!isMobile && (
1483
+ <> {lessons.length === 1 ? 'aula' : 'aulas'}</>
1484
+ )}
1485
+ </Badge>
1486
+ </TooltipTrigger>
1487
+ <TooltipContent side="bottom">
1488
+ Quantidade total de aulas do curso.
1489
+ </TooltipContent>
1490
+ </Tooltip>
1491
+
1492
+ <Tooltip>
1493
+ <TooltipTrigger asChild>
1494
+ <Badge
1495
+ variant="outline"
1496
+ className={cn(
1497
+ 'h-5 gap-1 font-normal bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30 cursor-help',
1498
+ isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
1499
+ )}
1500
+ >
1501
+ <Clock className="size-3" />
1502
+ {durationLabel}
1503
+ </Badge>
1504
+ </TooltipTrigger>
1505
+ <TooltipContent side="bottom">
1506
+ Duração somada de todas as aulas.
1507
+ </TooltipContent>
1508
+ </Tooltip>
1509
+
1510
+ <Tooltip>
1511
+ <TooltipTrigger asChild>
1512
+ <Badge
1513
+ className={cn(
1514
+ 'h-5 gap-1 font-normal bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 border-0 hover:bg-emerald-500/20 cursor-help',
1515
+ isMobile ? 'px-1 text-[0.6rem]' : 'px-1.5 text-[0.65rem]'
1516
+ )}
1517
+ >
1518
+ <span className="size-1.5 rounded-full bg-emerald-500 inline-block" />
1519
+ {publishedCount}
1520
+ {!isMobile && ' publicadas'}
1521
+ </Badge>
1522
+ </TooltipTrigger>
1523
+ <TooltipContent side="bottom">
1524
+ Quantidade de aulas visiveis para os alunos.
1525
+ </TooltipContent>
1526
+ </Tooltip>
1527
+
1528
+ <Tooltip>
1529
+ <TooltipTrigger asChild>
1530
+ <Badge
1531
+ variant="outline"
1532
+ className={cn(
1533
+ 'text-xs border-0 cursor-help',
1534
+ course.published
1535
+ ? 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-400 hover:bg-cyan-500/20'
1536
+ : 'bg-slate-500/15 text-slate-700 dark:text-slate-400 hover:bg-slate-500/20'
1537
+ )}
1538
+ >
1539
+ {course.published ? 'Publicado' : 'Rascunho'}
1540
+ </Badge>
1541
+ </TooltipTrigger>
1542
+ <TooltipContent side="bottom">
1543
+ Status atual do curso.
1544
+ </TooltipContent>
1545
+ </Tooltip>
1546
+ </TooltipProvider>
1547
+ </div>
962
1548
  </div>
963
1549
 
964
1550
  {/* ── Tabs ─────────────────────────────────────────────────────────── */}
965
1551
  <Tabs
966
1552
  value={activeTab}
967
1553
  onValueChange={setActiveTab}
968
- className="flex flex-col flex-1 min-h-0"
1554
+ className="flex flex-1 min-h-0 min-w-0 flex-col"
969
1555
  >
970
- <TabsList className="mx-3 mt-3 h-auto shrink-0 grid w-[calc(100%-1.5rem)] grid-cols-4 rounded-lg bg-muted/80 p-1">
971
- {(['estrutura', 'sobre', 'midia', 'extra'] as const).map((tab) => (
1556
+ <TabsList
1557
+ className={cn(
1558
+ 'mx-3 mt-3 h-auto shrink-0 w-[calc(100%-1.5rem)] rounded-lg bg-muted/80 p-1',
1559
+ 'flex items-center gap-1 overflow-x-auto overflow-y-hidden whitespace-nowrap',
1560
+ isMobile
1561
+ ? 'touch-pan-x snap-x snap-mandatory [scrollbar-width:thin] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted-foreground/35'
1562
+ : '[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden'
1563
+ )}
1564
+ >
1565
+ {(
1566
+ [
1567
+ 'estrutura',
1568
+ 'sobre',
1569
+ 'midia',
1570
+ 'recursos',
1571
+ 'extra',
1572
+ ...(showVideoTab ? ['videos'] : []),
1573
+ 'publicacao',
1574
+ ] as const
1575
+ ).map((tab) => (
972
1576
  <TabsTrigger
973
1577
  key={tab}
974
1578
  value={tab}
975
- className="h-8 px-2 text-xs font-medium"
1579
+ className={cn(
1580
+ 'shrink-0 font-medium',
1581
+ isMobile
1582
+ ? 'h-7 snap-start px-2.5 text-[11px]'
1583
+ : 'h-8 px-3 text-xs'
1584
+ )}
976
1585
  >
977
1586
  {tab === 'estrutura'
978
- ? 'Estrutura'
1587
+ ? t('structureEditor.tabs.structure')
979
1588
  : tab === 'sobre'
980
- ? 'Sobre'
1589
+ ? t('structureEditor.tabs.about')
981
1590
  : tab === 'midia'
982
- ? 'Mídia'
983
- : 'Extra'}
1591
+ ? t('structureEditor.tabs.media')
1592
+ : tab === 'recursos'
1593
+ ? t('structureEditor.tabs.resources')
1594
+ : tab === 'extra'
1595
+ ? t('structureEditor.tabs.extra')
1596
+ : tab === 'videos'
1597
+ ? t('structureEditor.tabs.videoProfiles')
1598
+ : t('structureEditor.tabs.publish')}
984
1599
  </TabsTrigger>
985
1600
  ))}
986
1601
  </TabsList>
987
1602
 
988
- <div className="flex-1 min-h-0 overflow-hidden">
989
- <ScrollArea className="h-full">
990
- <div className="p-3 flex flex-col gap-3">
991
- {/* ── Tab: Estrutura ──────────────────────────────────────── */}
992
- <TabsContent
993
- value="estrutura"
994
- className="mt-0 flex flex-col gap-3"
995
- >
996
- {/* Stat chips */}
997
- <Card className="bg-muted/20 py-2 gap-2">
998
- <CardHeader className="px-3 pt-2 pb-0">
999
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1000
- Resumo do conteúdo
1001
- </CardTitle>
1002
- </CardHeader>
1003
- <CardContent className="px-3 pb-2">
1004
- <div className="grid grid-cols-2 gap-2">
1005
- <StatChip
1006
- icon={<Layers className="size-3 text-blue-500" />}
1007
- label="Sessões"
1008
- value={sessions.length}
1009
- />
1010
- <StatChip
1011
- icon={<Video className="size-3 text-violet-500" />}
1012
- label="Aulas"
1013
- value={lessons.length}
1014
- />
1015
- <StatChip
1016
- icon={<Clock className="size-3 text-amber-500" />}
1017
- label="Duração"
1018
- value={
1019
- hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
1020
- }
1021
- />
1022
- <StatChip
1023
- icon={
1024
- <CheckCircle2 className="size-3 text-emerald-500" />
1025
- }
1026
- label="Publicadas"
1027
- value={publishedCount}
1028
- />
1029
- </div>
1030
- </CardContent>
1031
- </Card>
1032
-
1033
- {/* Dados principais */}
1034
- <Card className="bg-muted/20 py-2 gap-2">
1035
- <CardHeader className="px-3 pt-2 pb-0">
1036
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1037
- Dados principais
1038
- </CardTitle>
1039
- </CardHeader>
1040
- <CardContent className="px-3 pb-2 flex flex-col gap-3">
1603
+ <div className="flex-1 min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain">
1604
+ <div className="flex min-w-0 flex-col gap-3 p-2 sm:p-3">
1605
+ {/* ── Tab: Estrutura ──────────────────────────────────────── */}
1606
+ <TabsContent
1607
+ value="estrutura"
1608
+ className="mt-0 flex min-w-0 flex-col gap-3"
1609
+ >
1610
+ {/* Dados principais */}
1611
+ <Card className="bg-muted/20 py-2 gap-2">
1612
+ <CardHeader className="px-3 pt-2 pb-0">
1613
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1614
+ {t('structureEditor.structureTab.mainInfo.title')}
1615
+ </CardTitle>
1616
+ </CardHeader>
1617
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
1618
+ <FormField
1619
+ control={form.control}
1620
+ name="tituloComercial"
1621
+ render={({ field }) => (
1622
+ <FormItem>
1623
+ <FormLabel className="text-xs">
1624
+ {t(
1625
+ 'structureEditor.structureTab.mainInfo.fields.title'
1626
+ )}
1627
+ </FormLabel>
1628
+ <FormControl>
1629
+ <Input {...field} className="h-8 text-sm" />
1630
+ </FormControl>
1631
+ <FormMessage className="text-xs" />
1632
+ </FormItem>
1633
+ )}
1634
+ />
1635
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
1041
1636
  <FormField
1042
1637
  control={form.control}
1043
- name="tituloComercial"
1638
+ name="nomeInterno"
1044
1639
  render={({ field }) => (
1045
1640
  <FormItem>
1046
- <FormLabel className="text-xs">Título</FormLabel>
1641
+ <FormLabel className="text-xs">
1642
+ {t(
1643
+ 'structureEditor.structureTab.mainInfo.fields.internalName'
1644
+ )}
1645
+ </FormLabel>
1047
1646
  <FormControl>
1048
- <Input {...field} className="h-8 text-sm" />
1647
+ <Input {...field} className="h-8 text-xs" />
1049
1648
  </FormControl>
1050
1649
  <FormMessage className="text-xs" />
1051
1650
  </FormItem>
1052
1651
  )}
1053
1652
  />
1054
- <div className="grid grid-cols-2 gap-2">
1055
- <FormField
1056
- control={form.control}
1057
- name="nomeInterno"
1058
- render={({ field }) => (
1059
- <FormItem>
1060
- <FormLabel className="text-xs">
1061
- Nome Interno
1062
- </FormLabel>
1063
- <FormControl>
1064
- <Input {...field} className="h-8 text-xs" />
1065
- </FormControl>
1066
- <FormMessage className="text-xs" />
1067
- </FormItem>
1068
- )}
1069
- />
1070
- <FormField
1071
- control={form.control}
1072
- name="slug"
1073
- render={({ field }) => (
1074
- <FormItem>
1075
- <FormLabel className="text-xs">Slug</FormLabel>
1076
- <FormControl>
1077
- <Input
1078
- {...field}
1079
- className="h-8 text-xs font-mono"
1080
- onChange={(e) =>
1081
- field.onChange(e.target.value.toLowerCase())
1082
- }
1083
- />
1084
- </FormControl>
1085
- <FormMessage className="text-xs" />
1086
- </FormItem>
1087
- )}
1088
- />
1089
- </div>
1090
- </CardContent>
1091
- </Card>
1092
-
1093
- {/* Descrição */}
1094
- <Card className="bg-muted/20 py-2 gap-2">
1095
- <CardHeader className="px-3 pt-2 pb-0">
1096
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1097
- Descrição
1098
- </CardTitle>
1099
- </CardHeader>
1100
- <CardContent className="px-3 pb-2">
1101
1653
  <FormField
1102
1654
  control={form.control}
1103
- name="descricaoPublica"
1655
+ name="slug"
1104
1656
  render={({ field }) => (
1105
1657
  <FormItem>
1658
+ <FormLabel className="text-xs">
1659
+ {t(
1660
+ 'structureEditor.structureTab.mainInfo.fields.slug'
1661
+ )}
1662
+ </FormLabel>
1106
1663
  <FormControl>
1107
- <RichTextEditor
1108
- value={field.value}
1109
- onChange={field.onChange}
1664
+ <Input
1665
+ {...field}
1666
+ className="h-8 text-xs font-mono"
1667
+ onChange={(e) =>
1668
+ field.onChange(e.target.value.toLowerCase())
1669
+ }
1110
1670
  />
1111
1671
  </FormControl>
1112
1672
  <FormMessage className="text-xs" />
1113
1673
  </FormItem>
1114
1674
  )}
1115
1675
  />
1116
- </CardContent>
1117
- </Card>
1118
- </TabsContent>
1119
-
1120
- {/* ── Tab: Sobre ──────────────────────────────────────────── */}
1121
- <TabsContent value="sobre" className="mt-0 flex flex-col gap-3">
1122
- <CourseClassificationCard
1123
- form={form}
1124
- t={t}
1125
- levels={NIVEIS}
1126
- statuses={STATUS_OPTIONS}
1127
- offeringTypes={OFFERING_TYPE_OPTIONS}
1128
- />
1129
- <CourseRelationsCard
1130
- form={form}
1131
- t={t}
1132
- categoryOptions={categoryOptions}
1133
- instructorOptions={instructorOptions}
1134
- onCreateCategory={handleCreateCategory}
1135
- onCreateInstructor={handleCreateInstructor}
1136
- onEditCategory={handleEditCategory}
1137
- onEditInstructor={handleEditInstructor}
1138
- />
1139
- <CourseContentCard form={form} />
1140
- </TabsContent>
1141
-
1142
- {/* ── Tab: Mídia ──────────────────────────────────────────── */}
1143
- <TabsContent value="midia" className="mt-0">
1144
- <CourseMediaCard
1145
- logoPreview={logoPreview}
1146
- bannerPreview={bannerPreview}
1147
- uploadingLogo={uploadingLogo}
1148
- uploadingBanner={uploadingBanner}
1149
- onLogoSelect={(e: React.ChangeEvent<HTMLInputElement>) =>
1150
- handleFileSelect(e, setLogoPreview, 'logo')
1151
- }
1152
- onBannerSelect={(e: React.ChangeEvent<HTMLInputElement>) =>
1153
- handleFileSelect(e, setBannerPreview, 'banner')
1154
- }
1155
- logoFile={
1156
- apiCourse?.logoFileId
1157
- ? {
1158
- id: apiCourse.logoFileId,
1159
- name:
1160
- apiCourse.logoFilename ||
1161
- `#${apiCourse.logoFileId}`,
1676
+ </div>
1677
+ </CardContent>
1678
+ </Card>
1679
+
1680
+ {/* Descrição */}
1681
+ <Card className="bg-muted/20 py-2 gap-2">
1682
+ <CardHeader className="px-3 pt-2 pb-0">
1683
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1684
+ {t('structureEditor.structureTab.description.title')}
1685
+ </CardTitle>
1686
+ </CardHeader>
1687
+ <CardContent className="px-3 pb-2">
1688
+ <FormField
1689
+ control={form.control}
1690
+ name="descricaoPublica"
1691
+ render={({ field }) => (
1692
+ <FormItem>
1693
+ <FormControl>
1694
+ <RichTextEditor
1695
+ value={field.value}
1696
+ onChange={field.onChange}
1697
+ />
1698
+ </FormControl>
1699
+ <FormMessage className="text-xs" />
1700
+ </FormItem>
1701
+ )}
1702
+ />
1703
+ </CardContent>
1704
+ </Card>
1705
+ </TabsContent>
1706
+
1707
+ {/* ── Tab: Sobre ──────────────────────────────────────────── */}
1708
+ <TabsContent
1709
+ value="sobre"
1710
+ className="mt-0 flex min-w-0 flex-col gap-3"
1711
+ >
1712
+ <CourseClassificationCard
1713
+ form={form}
1714
+ t={t}
1715
+ levels={NIVEIS}
1716
+ statuses={STATUS_OPTIONS}
1717
+ offeringTypes={OFFERING_TYPE_OPTIONS}
1718
+ />
1719
+ <CourseRelationsCard
1720
+ form={form}
1721
+ t={t}
1722
+ categoryOptions={categoryOptions}
1723
+ projectOptions={projectOptions}
1724
+ instructorOptions={instructorOptions}
1725
+ onCreateCategory={handleCreateCategory}
1726
+ onCreateInstructor={handleCreateInstructor}
1727
+ onEditCategory={handleEditCategory}
1728
+ onEditInstructor={handleEditInstructor}
1729
+ />
1730
+ <CourseContentCard form={form} />
1731
+ </TabsContent>
1732
+
1733
+ {/* ── Tab: Mídia ──────────────────────────────────────────── */}
1734
+ <TabsContent value="midia" className="mt-0 min-w-0">
1735
+ <CourseMediaCard
1736
+ logoPreview={logoPreview}
1737
+ bannerPreview={bannerPreview}
1738
+ uploadingLogo={uploadingLogo}
1739
+ uploadingBanner={uploadingBanner}
1740
+ onLogoSelect={(e: React.ChangeEvent<HTMLInputElement>) =>
1741
+ handleFileSelect(e, setLogoPreview, 'logo')
1742
+ }
1743
+ onBannerSelect={(e: React.ChangeEvent<HTMLInputElement>) =>
1744
+ handleFileSelect(e, setBannerPreview, 'banner')
1745
+ }
1746
+ logoFile={
1747
+ apiCourse?.logoFileId
1748
+ ? {
1749
+ id: apiCourse.logoFileId,
1750
+ name:
1751
+ apiCourse.logoFilename ||
1752
+ `#${apiCourse.logoFileId}`,
1753
+ }
1754
+ : undefined
1755
+ }
1756
+ bannerFile={
1757
+ apiCourse?.bannerFileId
1758
+ ? {
1759
+ id: apiCourse.bannerFileId,
1760
+ name:
1761
+ apiCourse.bannerFilename ||
1762
+ `#${apiCourse.bannerFileId}`,
1763
+ }
1764
+ : undefined
1765
+ }
1766
+ onOpenLogoFile={() => openUploadedFile(apiCourse?.logoFileId)}
1767
+ onOpenBannerFile={() =>
1768
+ openUploadedFile(apiCourse?.bannerFileId)
1769
+ }
1770
+ onRemoveLogoFile={
1771
+ apiCourse?.logoFileId
1772
+ ? () => handleRemoveFile('logo')
1773
+ : undefined
1774
+ }
1775
+ onRemoveBannerFile={
1776
+ apiCourse?.bannerFileId
1777
+ ? () => handleRemoveFile('banner')
1778
+ : undefined
1779
+ }
1780
+ logoImageType={apiCourse?.logoImageType}
1781
+ bannerImageType={apiCourse?.bannerImageType}
1782
+ t={t}
1783
+ />
1784
+ </TabsContent>
1785
+
1786
+ {/* ── Tab: Recursos ─────────────────────────────────────── */}
1787
+ <TabsContent
1788
+ value="recursos"
1789
+ className="mt-0 flex min-w-0 flex-col gap-3"
1790
+ >
1791
+ <Card className="bg-muted/20 py-2 gap-2">
1792
+ <CardHeader className="px-3 pt-2 pb-0">
1793
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1794
+ {t('structureEditor.resources.title')}
1795
+ </CardTitle>
1796
+ </CardHeader>
1797
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
1798
+ <div
1799
+ role="button"
1800
+ tabIndex={0}
1801
+ aria-label={t(
1802
+ 'structureEditor.resources.dropzoneAriaLabel'
1803
+ )}
1804
+ aria-disabled={isUploadingResources || isSavingResources}
1805
+ onDragOver={(event) => {
1806
+ event.preventDefault();
1807
+ if (!isUploadingResources && !isSavingResources)
1808
+ setResourcesDragOver(true);
1809
+ }}
1810
+ onDragLeave={() => setResourcesDragOver(false)}
1811
+ onDrop={(event) => {
1812
+ event.preventDefault();
1813
+ setResourcesDragOver(false);
1814
+ if (!isUploadingResources && !isSavingResources) {
1815
+ void handleCourseResourceFiles(
1816
+ Array.from(event.dataTransfer.files)
1817
+ );
1818
+ }
1819
+ }}
1820
+ onClick={() => {
1821
+ if (!isUploadingResources && !isSavingResources) {
1822
+ resourcesInputRef.current?.click();
1823
+ }
1824
+ }}
1825
+ onKeyDown={(event) => {
1826
+ if (event.key === 'Enter' || event.key === ' ') {
1827
+ event.preventDefault();
1828
+ if (!isUploadingResources && !isSavingResources) {
1829
+ resourcesInputRef.current?.click();
1162
1830
  }
1163
- : undefined
1164
- }
1165
- bannerFile={
1166
- apiCourse?.bannerFileId
1167
- ? {
1168
- id: apiCourse.bannerFileId,
1169
- name:
1170
- apiCourse.bannerFilename ||
1171
- `#${apiCourse.bannerFileId}`,
1831
+ }
1832
+ }}
1833
+ className={cn(
1834
+ 'flex flex-col items-center justify-center gap-2 py-7 rounded-lg border-2 border-dashed transition-colors select-none',
1835
+ isUploadingResources || isSavingResources
1836
+ ? 'cursor-wait border-border opacity-60'
1837
+ : 'cursor-pointer',
1838
+ !isUploadingResources &&
1839
+ !isSavingResources &&
1840
+ resourcesDragOver
1841
+ ? 'border-primary/70 bg-primary/5'
1842
+ : !isUploadingResources && !isSavingResources
1843
+ ? 'border-border hover:border-primary/40 hover:bg-muted/30'
1844
+ : ''
1845
+ )}
1846
+ >
1847
+ {isUploadingResources || isSavingResources ? (
1848
+ <Loader2 className="size-7 animate-spin text-muted-foreground/50" />
1849
+ ) : (
1850
+ <UploadCloud
1851
+ className={cn(
1852
+ 'size-7 transition-colors',
1853
+ resourcesDragOver
1854
+ ? 'text-primary'
1855
+ : 'text-muted-foreground/50'
1856
+ )}
1857
+ />
1858
+ )}
1859
+ <div className="text-center">
1860
+ <p className="text-xs font-medium">
1861
+ {isUploadingResources
1862
+ ? t('structureEditor.resources.uploading')
1863
+ : isSavingResources
1864
+ ? t('structureEditor.resources.saving')
1865
+ : t('structureEditor.resources.idle')}
1866
+ </p>
1867
+ {!isUploadingResources && !isSavingResources && (
1868
+ <p className="text-xs text-muted-foreground">
1869
+ {t('structureEditor.resources.helper')}
1870
+ </p>
1871
+ )}
1872
+ </div>
1873
+ <input
1874
+ ref={resourcesInputRef}
1875
+ type="file"
1876
+ multiple
1877
+ className="hidden"
1878
+ onChange={(event) => {
1879
+ if (event.target.files) {
1880
+ void handleCourseResourceFiles(
1881
+ Array.from(event.target.files)
1882
+ );
1883
+ event.target.value = '';
1172
1884
  }
1173
- : undefined
1174
- }
1175
- onOpenLogoFile={() =>
1176
- openUploadedFile(apiCourse?.logoFileId)
1177
- }
1178
- onOpenBannerFile={() =>
1179
- openUploadedFile(apiCourse?.bannerFileId)
1180
- }
1181
- onRemoveLogoFile={
1182
- apiCourse?.logoFileId
1183
- ? () => handleRemoveFile('logo')
1184
- : undefined
1185
- }
1186
- onRemoveBannerFile={
1187
- apiCourse?.bannerFileId
1188
- ? () => handleRemoveFile('banner')
1189
- : undefined
1190
- }
1191
- logoImageType={apiCourse?.logoImageType}
1192
- bannerImageType={apiCourse?.bannerImageType}
1193
- t={t}
1194
- />
1195
- </TabsContent>
1885
+ }}
1886
+ />
1887
+ </div>
1888
+
1889
+ {courseResources.length === 0 ? (
1890
+ <p className="text-center text-xs text-muted-foreground py-1">
1891
+ {t('structureEditor.resources.empty')}
1892
+ </p>
1893
+ ) : (
1894
+ <div className="flex flex-col gap-1">
1895
+ {courseResources.map((item) => {
1896
+ const ItemIcon = getResourceIcon(item.type);
1897
+ return (
1898
+ <div
1899
+ key={item.id}
1900
+ className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
1901
+ >
1902
+ <ItemIcon
1903
+ className={cn(
1904
+ 'size-3.5 shrink-0',
1905
+ getResourceIconColor(item.type)
1906
+ )}
1907
+ />
1908
+ <div className="flex-1 min-w-0">
1909
+ <p className="text-xs truncate font-medium">
1910
+ {item.name}
1911
+ </p>
1912
+ <p className="text-[0.65rem] text-muted-foreground">
1913
+ {item.size ||
1914
+ t('structureEditor.resources.attachedFile')}
1915
+ </p>
1916
+ </div>
1917
+ <Button
1918
+ type="button"
1919
+ variant="ghost"
1920
+ size="icon"
1921
+ className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
1922
+ onClick={() =>
1923
+ void handleDownloadCourseResource(item)
1924
+ }
1925
+ aria-label={t(
1926
+ 'structureEditor.resources.downloadAria',
1927
+ { name: item.name }
1928
+ )}
1929
+ >
1930
+ <Download className="size-3" />
1931
+ </Button>
1932
+ <Button
1933
+ type="button"
1934
+ variant="ghost"
1935
+ size="icon"
1936
+ className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
1937
+ onClick={() =>
1938
+ void handleRemoveCourseResource(item)
1939
+ }
1940
+ aria-label={t(
1941
+ 'structureEditor.resources.removeAria',
1942
+ { name: item.name }
1943
+ )}
1944
+ >
1945
+ <X className="size-3" />
1946
+ </Button>
1947
+ </div>
1948
+ );
1949
+ })}
1950
+ </div>
1951
+ )}
1952
+ </CardContent>
1953
+ </Card>
1954
+ </TabsContent>
1955
+
1956
+ {/* ── Tab: Extra ──────────────────────────────────────────── */}
1957
+ <TabsContent
1958
+ value="extra"
1959
+ className="mt-0 flex min-w-0 flex-col gap-3"
1960
+ >
1961
+ <CourseCertificateCard
1962
+ form={form}
1963
+ t={t}
1964
+ options={certificateOptions}
1965
+ onCreateTemplate={handleCreateCertificateTemplate}
1966
+ />
1967
+ <CourseFlagsCard form={form} t={t} />
1968
+ <CourseDangerZoneCard
1969
+ t={t}
1970
+ onDelete={() => setDeleteDialogOpen(true)}
1971
+ />
1972
+ </TabsContent>
1973
+
1974
+ {/* ── Tab: Vídeos (on_demand / blended only) ─────────────── */}
1975
+ {showVideoTab && (
1976
+ <TabsContent
1977
+ value="videos"
1978
+ className="mt-0 flex min-w-0 flex-col gap-3"
1979
+ >
1980
+ <Card className="bg-muted/20 py-2 gap-2">
1981
+ <CardHeader className="px-3 pt-2 pb-0">
1982
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1983
+ {t('structureEditor.videoProfiles.title')}
1984
+ </CardTitle>
1985
+ </CardHeader>
1986
+ <CardContent className="px-3 pb-2 flex flex-col gap-2">
1987
+ <EntityPicker
1988
+ key={videoProfilePickerResetKey}
1989
+ placeholder={t(
1990
+ 'structureEditor.videoProfiles.placeholder'
1991
+ )}
1992
+ options={availableVideoProfiles}
1993
+ getOptionValue={(opt) => opt.id}
1994
+ getOptionLabel={(opt) => opt.name}
1995
+ onChange={(_val, opt) => {
1996
+ if (opt && !linkedProfileIds.includes(opt.id)) {
1997
+ setLinkedProfileIds((prev) => [...prev, opt.id]);
1998
+ setVideoProfilePickerResetKey(
1999
+ (current) => current + 1
2000
+ );
2001
+ }
2002
+ }}
2003
+ showCreateButton
2004
+ createTitle={t(
2005
+ 'structureEditor.videoProfiles.createTitle'
2006
+ )}
2007
+ createFields={[
2008
+ {
2009
+ name: 'name',
2010
+ label: t(
2011
+ 'structureEditor.videoProfiles.createFields.name.label'
2012
+ ),
2013
+ placeholder: t(
2014
+ 'structureEditor.videoProfiles.createFields.name.placeholder'
2015
+ ),
2016
+ required: true,
2017
+ },
2018
+ {
2019
+ name: 'ffmpeg_params',
2020
+ label: t(
2021
+ 'structureEditor.videoProfiles.createFields.ffmpegParams.label'
2022
+ ),
2023
+ placeholder: t(
2024
+ 'structureEditor.videoProfiles.createFields.ffmpegParams.placeholder'
2025
+ ),
2026
+ required: true,
2027
+ },
2028
+ ]}
2029
+ onCreate={async (values) => {
2030
+ try {
2031
+ const response = await request<VideoProfileOption>({
2032
+ url: '/lms/video-resolution-profiles',
2033
+ method: 'POST',
2034
+ data: {
2035
+ name: values.name,
2036
+ ffmpeg_params: values.ffmpeg_params,
2037
+ },
2038
+ });
2039
+ await refetchVideoProfiles();
2040
+ return response.data;
2041
+ } catch {
2042
+ toast.error(
2043
+ t('structureEditor.videoProfiles.createError')
2044
+ );
2045
+ return null;
2046
+ }
2047
+ }}
2048
+ />
1196
2049
 
1197
- {/* ── Tab: Extra ──────────────────────────────────────────── */}
1198
- <TabsContent value="extra" className="mt-0 flex flex-col gap-3">
1199
- <CourseCertificateCard
1200
- form={form}
1201
- t={t}
1202
- options={certificateOptions}
1203
- onCreateTemplate={handleCreateCertificateTemplate}
1204
- />
1205
- <CourseFlagsCard form={form} t={t} />
1206
- <CourseDangerZoneCard
1207
- t={t}
1208
- onDelete={() => setDeleteDialogOpen(true)}
1209
- />
2050
+ {isFetchingVideoProfiles ? (
2051
+ <div className="flex flex-col gap-1">
2052
+ {Array.from({ length: 3 }).map((_, index) => (
2053
+ <div
2054
+ key={`video-profile-skeleton-${index}`}
2055
+ className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
2056
+ >
2057
+ <Skeleton className="size-3.5 rounded-full shrink-0" />
2058
+ <Skeleton className="h-3 w-full max-w-56" />
2059
+ </div>
2060
+ ))}
2061
+ </div>
2062
+ ) : linkedProfileIds.length === 0 ? (
2063
+ <p className="text-center text-xs text-muted-foreground py-1">
2064
+ {t('structureEditor.videoProfiles.empty')}
2065
+ </p>
2066
+ ) : (
2067
+ <div className="flex flex-col gap-1">
2068
+ {linkedProfileIds.map((profileId) => {
2069
+ const profile = allVideoProfiles.find(
2070
+ (p) => p.id === profileId
2071
+ );
2072
+ return (
2073
+ <div
2074
+ key={profileId}
2075
+ className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
2076
+ >
2077
+ <Video className="size-3.5 shrink-0 text-violet-500" />
2078
+ <div className="flex-1 min-w-0">
2079
+ <p className="text-xs font-medium truncate">
2080
+ {profile?.name?.trim()
2081
+ ? profile.name
2082
+ : t(
2083
+ 'structureEditor.videoProfiles.fallbackName',
2084
+ {
2085
+ id: profileId,
2086
+ }
2087
+ )}
2088
+ </p>
2089
+ </div>
2090
+ <Button
2091
+ type="button"
2092
+ variant="ghost"
2093
+ size="icon"
2094
+ className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
2095
+ onClick={() =>
2096
+ handleEditVideoProfile(profileId)
2097
+ }
2098
+ aria-label={t(
2099
+ 'structureEditor.videoProfiles.editAria'
2100
+ )}
2101
+ >
2102
+ <Pencil className="size-3" />
2103
+ </Button>
2104
+ <Button
2105
+ type="button"
2106
+ variant="ghost"
2107
+ size="icon"
2108
+ className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
2109
+ onClick={() =>
2110
+ setLinkedProfileIds((prev) =>
2111
+ prev.filter((id) => id !== profileId)
2112
+ )
2113
+ }
2114
+ aria-label={t(
2115
+ 'structureEditor.videoProfiles.removeAria'
2116
+ )}
2117
+ >
2118
+ <X className="size-3" />
2119
+ </Button>
2120
+ </div>
2121
+ );
2122
+ })}
2123
+ </div>
2124
+ )}
2125
+ </CardContent>
2126
+ </Card>
1210
2127
  </TabsContent>
1211
- </div>
1212
- </ScrollArea>
2128
+ )}
2129
+
2130
+ {/* ── Tab: Publicação ─────────────────────────────────────── */}
2131
+ <TabsContent
2132
+ value="publicacao"
2133
+ className="mt-0 flex min-w-0 flex-col gap-3"
2134
+ >
2135
+ <Card className="bg-muted/20 py-2 gap-2">
2136
+ <CardHeader className="px-3 pt-2 pb-0">
2137
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
2138
+ {t('structureEditor.publishChecklist.title')}
2139
+ </CardTitle>
2140
+ </CardHeader>
2141
+ <CardContent className="px-3 pb-2 flex flex-col gap-2">
2142
+ {checklistItems.map((item) => (
2143
+ <div
2144
+ key={item.id}
2145
+ className="flex items-center gap-2 rounded-md border bg-background/80 px-2.5 py-2"
2146
+ >
2147
+ {item.done ? (
2148
+ <CheckCircle2 className="size-4 shrink-0 text-emerald-500" />
2149
+ ) : (
2150
+ <AlertTriangle className="size-4 shrink-0 text-amber-500" />
2151
+ )}
2152
+ <div className="flex-1 min-w-0">
2153
+ <p className="text-xs font-medium truncate">
2154
+ {item.label}
2155
+ </p>
2156
+ {!item.done && (
2157
+ <p className="text-[0.65rem] text-muted-foreground">
2158
+ {item.required
2159
+ ? t('structureEditor.publishChecklist.required')
2160
+ : t(
2161
+ 'structureEditor.publishChecklist.recommended'
2162
+ )}
2163
+ </p>
2164
+ )}
2165
+ </div>
2166
+ <Badge
2167
+ variant={item.done ? 'default' : 'secondary'}
2168
+ className="text-[0.65rem]"
2169
+ >
2170
+ {item.done
2171
+ ? t('structureEditor.publishChecklist.done')
2172
+ : t('structureEditor.publishChecklist.missing')}
2173
+ </Badge>
2174
+ </div>
2175
+ ))}
2176
+ </CardContent>
2177
+ </Card>
2178
+
2179
+ <Card className="bg-muted/20 py-2 gap-2">
2180
+ <CardHeader className="px-3 pt-2 pb-0">
2181
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
2182
+ {t('structureEditor.publishChecklist.statusTitle')}
2183
+ </CardTitle>
2184
+ </CardHeader>
2185
+ <CardContent className="px-3 pb-2 flex items-center justify-between gap-2">
2186
+ <p className="text-xs text-muted-foreground">
2187
+ {t('structureEditor.publishChecklist.statusProgress', {
2188
+ completed: completedRequired,
2189
+ total: requiredChecklist.length,
2190
+ })}
2191
+ </p>
2192
+ <Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
2193
+ {isReadyToPublish
2194
+ ? t('structureEditor.publishChecklist.ready')
2195
+ : t('structureEditor.publishChecklist.notReady')}
2196
+ </Badge>
2197
+ </CardContent>
2198
+ </Card>
2199
+ </TabsContent>
2200
+ </div>
1213
2201
  </div>
1214
2202
  </Tabs>
1215
2203
 
@@ -1226,7 +2214,7 @@ export function EditorCourse() {
1226
2214
  onClick={() => form.reset()}
1227
2215
  >
1228
2216
  <Undo2 className="size-3 mr-1" />
1229
- Cancelar
2217
+ {t('structureEditor.footer.cancel')}
1230
2218
  </Button>
1231
2219
  <div className="flex-1" />
1232
2220
  <Button
@@ -1238,7 +2226,7 @@ export function EditorCourse() {
1238
2226
  onClick={() => createSessionMutation.mutate()}
1239
2227
  >
1240
2228
  <Plus className="size-3 mr-1" />
1241
- Nova sessão
2229
+ {t('structureEditor.footer.newSession')}
1242
2230
  </Button>
1243
2231
  <Button
1244
2232
  type="submit"
@@ -1251,7 +2239,7 @@ export function EditorCourse() {
1251
2239
  ) : (
1252
2240
  <Save className="size-3 mr-1" />
1253
2241
  )}
1254
- Salvar
2242
+ {t('structureEditor.footer.save')}
1255
2243
  </Button>
1256
2244
  </div>
1257
2245
  </div>
@@ -1284,6 +2272,68 @@ export function EditorCourse() {
1284
2272
  }}
1285
2273
  />
1286
2274
 
2275
+ <Sheet
2276
+ open={videoProfileEditSheetOpen}
2277
+ onOpenChange={(open) => {
2278
+ setVideoProfileEditSheetOpen(open);
2279
+ if (!open) setEditingVideoProfileId(null);
2280
+ }}
2281
+ >
2282
+ <ResizableSheetContent
2283
+ sheetId="lms-course-structure-video-profile-edit-sheet"
2284
+ defaultWidth={560}
2285
+ minWidth={420}
2286
+ maxWidth={920}
2287
+ className="sm:max-w-lg"
2288
+ >
2289
+ <SheetHeader>
2290
+ <SheetTitle>
2291
+ {t('structureEditor.videoProfiles.sheet.title')}
2292
+ </SheetTitle>
2293
+ <SheetDescription>
2294
+ {t('structureEditor.videoProfiles.sheet.description')}
2295
+ </SheetDescription>
2296
+ </SheetHeader>
2297
+ <div className="space-y-3 px-4 pb-4 pt-2">
2298
+ <div className="space-y-1.5">
2299
+ <FormLabel>
2300
+ {t('structureEditor.videoProfiles.createFields.name.label')}
2301
+ </FormLabel>
2302
+ <Input
2303
+ value={editingVideoProfileName}
2304
+ onChange={(e) => setEditingVideoProfileName(e.target.value)}
2305
+ placeholder={t(
2306
+ 'structureEditor.videoProfiles.createFields.name.placeholder'
2307
+ )}
2308
+ />
2309
+ </div>
2310
+ <FfmpegParamsEditor
2311
+ value={editingVideoProfileParams}
2312
+ onChange={setEditingVideoProfileParams}
2313
+ />
2314
+ <div className="flex justify-end gap-2 pt-2">
2315
+ <Button
2316
+ type="button"
2317
+ variant="outline"
2318
+ onClick={() => setVideoProfileEditSheetOpen(false)}
2319
+ disabled={savingVideoProfileEdit}
2320
+ >
2321
+ {t('structureEditor.videoProfiles.sheet.actions.cancel')}
2322
+ </Button>
2323
+ <Button
2324
+ type="button"
2325
+ disabled={savingVideoProfileEdit}
2326
+ onClick={() => void handleSaveVideoProfileEdit()}
2327
+ >
2328
+ {savingVideoProfileEdit
2329
+ ? t('structureEditor.videoProfiles.sheet.actions.saving')
2330
+ : t('structureEditor.videoProfiles.sheet.actions.save')}
2331
+ </Button>
2332
+ </div>
2333
+ </div>
2334
+ </ResizableSheetContent>
2335
+ </Sheet>
2336
+
1287
2337
  <Sheet
1288
2338
  open={categoryEditSheetOpen}
1289
2339
  onOpenChange={(open) => {
@@ -1300,7 +2350,13 @@ export function EditorCourse() {
1300
2350
  }
1301
2351
  }}
1302
2352
  >
1303
- <SheetContent className="sm:max-w-lg">
2353
+ <ResizableSheetContent
2354
+ sheetId="lms-course-structure-category-edit-sheet"
2355
+ defaultWidth={560}
2356
+ minWidth={420}
2357
+ maxWidth={920}
2358
+ className="sm:max-w-lg"
2359
+ >
1304
2360
  <SheetHeader>
1305
2361
  <SheetTitle>Editar categoria</SheetTitle>
1306
2362
  <SheetDescription>
@@ -1350,7 +2406,7 @@ export function EditorCourse() {
1350
2406
  Salvar
1351
2407
  </Button>
1352
2408
  </SheetFooter>
1353
- </SheetContent>
2409
+ </ResizableSheetContent>
1354
2410
  </Sheet>
1355
2411
 
1356
2412
  {/* ── Delete dialog ────────────────────────────────────────────────── */}