@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,10 +9,8 @@ import {
9
9
  SearchBar,
10
10
  ViewModeToggle,
11
11
  } from '@/components/entity-list';
12
- import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
13
12
  import { Badge } from '@/components/ui/badge';
14
13
  import { Button } from '@/components/ui/button';
15
- import { Calendar } from '@/components/ui/calendar';
16
14
  import { Card, CardContent } from '@/components/ui/card';
17
15
  import {
18
16
  Dialog,
@@ -29,35 +27,7 @@ import {
29
27
  DropdownMenuSeparator,
30
28
  DropdownMenuTrigger,
31
29
  } from '@/components/ui/dropdown-menu';
32
- import { EntityPicker } from '@/components/ui/entity-picker';
33
- import {
34
- Field,
35
- FieldDescription,
36
- FieldError,
37
- FieldLabel,
38
- } from '@/components/ui/field';
39
- import { Input } from '@/components/ui/input';
40
30
  import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
41
- import {
42
- Popover,
43
- PopoverContent,
44
- PopoverTrigger,
45
- } from '@/components/ui/popover';
46
- import {
47
- Select,
48
- SelectContent,
49
- SelectItem,
50
- SelectTrigger,
51
- SelectValue,
52
- } from '@/components/ui/select';
53
- import {
54
- Sheet,
55
- SheetContent,
56
- SheetDescription,
57
- SheetFooter,
58
- SheetHeader,
59
- SheetTitle,
60
- } from '@/components/ui/sheet';
61
31
  import { Skeleton } from '@/components/ui/skeleton';
62
32
  import {
63
33
  Table,
@@ -67,11 +37,10 @@ import {
67
37
  TableHeader,
68
38
  TableRow,
69
39
  } from '@/components/ui/table';
40
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
70
41
  import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
71
42
  import { formatDate as formatDateLocalized } from '@/lib/format-date';
72
43
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
73
- import { zodResolver } from '@hookform/resolvers/zod';
74
- import { format } from 'date-fns';
75
44
  import { motion } from 'framer-motion';
76
45
  import {
77
46
  AlertTriangle,
@@ -95,19 +64,8 @@ import {
95
64
  import { useTranslations } from 'next-intl';
96
65
  import { useRouter } from 'next/navigation';
97
66
  import { useEffect, useMemo, useRef, useState } from 'react';
98
- import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
99
- import type { DateRange } from 'react-day-picker';
100
- import { Controller, useForm, useWatch } from 'react-hook-form';
101
67
  import { toast } from 'sonner';
102
- import { z } from 'zod';
103
- import {
104
- CourseFormSheet,
105
- DEFAULT_COURSE_FORM_VALUES,
106
- getCourseSheetSchema,
107
- type CourseCategoryOption,
108
- type CourseSheetFormValues,
109
- } from '../_components/course-form-sheet';
110
- import { CreateLmsPersonSheet } from '../_components/create-lms-person-sheet';
68
+ import { ClassFormSheet } from '../_components/class-form-sheet';
111
69
 
112
70
  // ── Types ─────────────────────────────────────────────────────────────────────
113
71
 
@@ -117,6 +75,7 @@ interface Turma {
117
75
  curso: string;
118
76
  cursoId: number;
119
77
  instructorId?: number | null;
78
+ professorAvatarId?: number | null;
120
79
  primaryColor?: string | null;
121
80
  tipo: 'presencial' | 'online' | 'hibrida';
122
81
  dataInicio: string;
@@ -133,14 +92,6 @@ interface Turma {
133
92
  }
134
93
 
135
94
  type SessionRecurrenceFrequency = 'daily' | 'weekly' | 'monthly' | 'yearly';
136
- type SessionRecurrenceMode =
137
- | 'none'
138
- | 'daily'
139
- | 'weekly'
140
- | 'monthly'
141
- | 'yearly'
142
- | 'weekdays'
143
- | 'custom';
144
95
  type SessionRecurrenceDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
145
96
 
146
97
  type SessionRecurrenceSummary = {
@@ -165,6 +116,7 @@ type ApiClass = {
165
116
  capacity: number;
166
117
  courseId: number;
167
118
  instructorId?: number | null;
119
+ instructorAvatarId?: number | null;
168
120
  courseTitle: string;
169
121
  enrolledCount: number;
170
122
  professor?: string | null;
@@ -203,107 +155,12 @@ type ApiCourseList = {
203
155
  lastPage?: number;
204
156
  };
205
157
 
206
- type ApiCategory = {
207
- id: number;
208
- slug: string;
209
- name: string;
210
- status?: 'active' | 'inactive';
211
- };
212
-
213
- type ApiCategoryList = {
214
- data: ApiCategory[];
215
- total: number;
216
- page: number;
217
- pageSize: number;
218
- };
219
-
220
- type ApiCreatedCourse = {
221
- id: number;
222
- title: string;
223
- };
224
-
225
- type InstructorOption = {
226
- id: number;
227
- name: string;
228
- personId?: number;
229
- avatarId?: number | null;
230
- userPhotoId?: number | null;
231
- qualificationSlugs?: string[];
232
- };
233
-
234
- type InstructorApiRow = {
235
- id?: number | string;
236
- instructor_id?: number | string;
237
- value?: number | string;
238
- name?: string;
239
- nome?: string;
240
- full_name?: string;
241
- label?: string;
242
- personId?: number | string;
243
- person_id?: number | string;
244
- avatarId?: number | string | null;
245
- avatar_id?: number | string | null;
246
- userPhotoId?: number | string | null;
247
- user_photo_id?: number | string | null;
248
- qualificationSlugs?: string[];
249
- };
250
-
251
158
  type Locale = {
252
159
  id?: number;
253
160
  code: string;
254
161
  name: string;
255
162
  };
256
163
 
257
- function normalizeInstructorOption(
258
- item: InstructorApiRow
259
- ): InstructorOption | null {
260
- const id = Number(item?.id ?? item?.instructor_id ?? item?.value ?? 0);
261
- const name = String(
262
- item?.name ?? item?.nome ?? item?.full_name ?? item?.label ?? ''
263
- ).trim();
264
-
265
- if (!id || !name) {
266
- return null;
267
- }
268
-
269
- const rawAvatarId = item?.avatarId ?? item?.avatar_id;
270
- const avatarId =
271
- rawAvatarId !== undefined && rawAvatarId !== null
272
- ? Number(rawAvatarId) || null
273
- : null;
274
- const rawUserPhotoId = item?.userPhotoId ?? item?.user_photo_id;
275
- const userPhotoId =
276
- rawUserPhotoId !== undefined && rawUserPhotoId !== null
277
- ? Number(rawUserPhotoId) || null
278
- : null;
279
-
280
- return {
281
- id,
282
- name,
283
- personId: Number(item?.personId ?? item?.person_id ?? 0) || undefined,
284
- avatarId,
285
- userPhotoId,
286
- qualificationSlugs: Array.isArray(item?.qualificationSlugs)
287
- ? item.qualificationSlugs
288
- : undefined,
289
- };
290
- }
291
-
292
- function getInstructorAvatarUrl(option?: {
293
- userPhotoId?: number | null;
294
- avatarId?: number | null;
295
- }) {
296
- if (option?.userPhotoId) {
297
- return `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${option.userPhotoId}`;
298
- }
299
-
300
- if (option?.avatarId) {
301
- return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`;
302
- }
303
-
304
- return undefined;
305
- }
306
-
307
164
  function getCourseIdByTitle(
308
165
  courses: Array<{ id: number; title: string }>,
309
166
  title: string
@@ -311,59 +168,6 @@ function getCourseIdByTitle(
311
168
  return courses.find((course) => course.title === title)?.id;
312
169
  }
313
170
 
314
- function toApiCourseLevel(level: CourseSheetFormValues['nivel']) {
315
- if (level === 'iniciante') return 'beginner';
316
- if (level === 'intermediario') return 'intermediate';
317
- return 'advanced';
318
- }
319
-
320
- function toApiCourseStatus(status: CourseSheetFormValues['status']) {
321
- if (status === 'ativo') return 'published';
322
- if (status === 'rascunho') return 'draft';
323
- return 'archived';
324
- }
325
-
326
- function getContrastColor(hex: string) {
327
- const cleaned = hex.replace('#', '');
328
- if (cleaned.length !== 6) return '#FFFFFF';
329
-
330
- const r = parseInt(cleaned.slice(0, 2), 16);
331
- const g = parseInt(cleaned.slice(2, 4), 16);
332
- const b = parseInt(cleaned.slice(4, 6), 16);
333
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
334
-
335
- return luminance > 0.6 ? '#111827' : '#FFFFFF';
336
- }
337
-
338
- function parseFormDate(value: string) {
339
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '');
340
-
341
- if (!match) {
342
- return null;
343
- }
344
-
345
- return new Date(
346
- Number(match[1]),
347
- Number(match[2]) - 1,
348
- Number(match[3]),
349
- 12,
350
- 0,
351
- 0,
352
- 0
353
- );
354
- }
355
-
356
- function getDayCodeFromDate(value?: string): SessionRecurrenceDay {
357
- const date = parseFormDate(value ?? '') ?? new Date();
358
- return ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][
359
- date.getDay()
360
- ] as SessionRecurrenceDay;
361
- }
362
-
363
- function getDefaultSessionTitle(courseTitle?: string, code?: string) {
364
- return [courseTitle?.trim(), code?.trim()].filter(Boolean).join(' - ');
365
- }
366
-
367
171
  function toApiType(tipo: string) {
368
172
  if (tipo === 'presencial') return 'presential';
369
173
  if (tipo === 'hibrida') return 'hybrid';
@@ -416,6 +220,7 @@ function mapApiClass(item: ApiClass): Turma {
416
220
  curso: item.courseTitle,
417
221
  cursoId: item.courseId,
418
222
  instructorId: item.instructorId ?? null,
223
+ professorAvatarId: item.instructorAvatarId ?? null,
419
224
  primaryColor: item.primaryColor,
420
225
  tipo: toPtType(item.deliveryMode),
421
226
  dataInicio: getDateOnly(item.startDate),
@@ -431,130 +236,6 @@ function mapApiClass(item: ApiClass): Turma {
431
236
  };
432
237
  }
433
238
 
434
- // ── Schema ────────────────────────────────────────────────────────────────────
435
-
436
- function getTurmaSchema(t: (key: string) => string) {
437
- return z
438
- .object({
439
- codigo: z.string().min(3, t('form.validation.codigoMinLength')),
440
- curso: z.string().optional(),
441
- courseId: z.coerce
442
- .number({
443
- invalid_type_error: t('form.validation.cursoRequired'),
444
- })
445
- .int()
446
- .positive(t('form.validation.cursoRequired')),
447
- tipo: z.string().min(1, t('form.validation.tipoRequired')),
448
- professor: z.string().min(3, t('form.validation.professorMinLength')),
449
- vagas: z.coerce.number().min(1, t('form.validation.vagasMin')),
450
- dataInicio: z.string().min(1, t('form.validation.dataInicioRequired')),
451
- dataFim: z.string().min(1, t('form.validation.dataFimRequired')),
452
- horarioInicio: z
453
- .string()
454
- .min(1, t('form.validation.horarioInicioRequired'))
455
- .regex(
456
- /^([01]\d|2[0-3]):([0-5]\d)$/,
457
- t('form.validation.horarioFormato')
458
- ),
459
- horarioFim: z
460
- .string()
461
- .min(1, t('form.validation.horarioFimRequired'))
462
- .regex(
463
- /^([01]\d|2[0-3]):([0-5]\d)$/,
464
- t('form.validation.horarioFormato')
465
- ),
466
- sessionRecurrenceMode: z
467
- .enum([
468
- 'none',
469
- 'daily',
470
- 'weekly',
471
- 'monthly',
472
- 'yearly',
473
- 'weekdays',
474
- 'custom',
475
- ] as const)
476
- .default('none'),
477
- sessionRecurrenceCustomFrequency: z
478
- .enum(['daily', 'weekly', 'monthly', 'yearly'] as const)
479
- .default('weekly'),
480
- sessionRecurrenceInterval: z.coerce
481
- .number()
482
- .min(1, t('form.validation.sessionRecurrenceIntervalMin'))
483
- .default(1),
484
- sessionRecurrenceDaysOfWeek: z
485
- .array(z.enum(['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] as const))
486
- .default([]),
487
- sessionRecurrenceUntil: z.string().optional(),
488
- sessionTitleMode: z
489
- .enum(['default-course-code', 'custom'] as const)
490
- .default('default-course-code'),
491
- sessionTitle: z.string().optional(),
492
- status: z.string().min(1, t('form.validation.statusRequired')),
493
- instructorId: z.number().int().positive().optional(),
494
- })
495
- .superRefine((values, ctx) => {
496
- if (
497
- values.dataInicio &&
498
- values.dataFim &&
499
- values.dataFim < values.dataInicio
500
- ) {
501
- ctx.addIssue({
502
- code: z.ZodIssueCode.custom,
503
- path: ['dataFim'],
504
- message: 'A data final nao pode ser anterior a data inicial.',
505
- });
506
- }
507
-
508
- if (
509
- values.horarioInicio &&
510
- values.horarioFim &&
511
- values.horarioFim < values.horarioInicio
512
- ) {
513
- ctx.addIssue({
514
- code: z.ZodIssueCode.custom,
515
- path: ['horarioFim'],
516
- message: 'O horario final nao pode ser anterior ao horario inicial.',
517
- });
518
- }
519
-
520
- const requiresDays =
521
- values.sessionRecurrenceMode === 'weekly' ||
522
- values.sessionRecurrenceMode === 'weekdays' ||
523
- (values.sessionRecurrenceMode === 'custom' &&
524
- values.sessionRecurrenceCustomFrequency === 'weekly');
525
-
526
- if (requiresDays && values.sessionRecurrenceDaysOfWeek.length === 0) {
527
- ctx.addIssue({
528
- code: z.ZodIssueCode.custom,
529
- path: ['sessionRecurrenceDaysOfWeek'],
530
- message: t('form.validation.sessionRecurrenceDaysRequired'),
531
- });
532
- }
533
- });
534
- }
535
-
536
- type TurmaForm = {
537
- codigo: string;
538
- curso: string;
539
- courseId?: number;
540
- instructorId?: number;
541
- tipo: string;
542
- professor: string;
543
- vagas: number;
544
- dataInicio: string;
545
- dataFim: string;
546
- horarioInicio: string;
547
- horarioFim: string;
548
- sessionRecurrenceMode: SessionRecurrenceMode;
549
- sessionRecurrenceCustomFrequency: SessionRecurrenceFrequency;
550
- sessionRecurrenceInterval: number;
551
- sessionRecurrenceDaysOfWeek: SessionRecurrenceDay[];
552
- sessionRecurrenceUntil?: string;
553
- sessionTitleMode: 'default-course-code' | 'custom';
554
- sessionTitle?: string;
555
- status: string;
556
- };
557
-
558
239
  type ViewMode = 'cards' | 'list';
559
240
 
560
241
  // ── Constants ─────────────────────────────────────────────────────────────────
@@ -569,6 +250,26 @@ const STATUS_VARIANT: Record<
569
250
  cancelada: 'destructive',
570
251
  };
571
252
 
253
+ const STATUS_CLASS: Record<string, string> = {
254
+ aberta:
255
+ 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950 dark:text-emerald-400 dark:border-emerald-800',
256
+ em_andamento:
257
+ 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-400 dark:border-blue-800',
258
+ concluida:
259
+ 'bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700',
260
+ cancelada:
261
+ 'bg-red-100 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-400 dark:border-red-800',
262
+ };
263
+
264
+ const TIPO_CLASS: Record<string, string> = {
265
+ presencial:
266
+ 'bg-sky-100 text-sky-700 border-sky-200 dark:bg-sky-950 dark:text-sky-400 dark:border-sky-800',
267
+ online:
268
+ 'bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-950 dark:text-violet-400 dark:border-violet-800',
269
+ hibrida:
270
+ 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-950 dark:text-amber-400 dark:border-amber-800',
271
+ };
272
+
572
273
  const TIPO_ICON: Record<string, LucideIcon> = {
573
274
  presencial: MapPin,
574
275
  online: Monitor,
@@ -576,103 +277,6 @@ const TIPO_ICON: Record<string, LucideIcon> = {
576
277
  };
577
278
 
578
279
  const PAGE_SIZES = [6, 12, 24];
579
- const TIME_OPTIONS = Array.from({ length: 32 }, (_, index) => {
580
- const hour = 6 + Math.floor(index / 2);
581
- const minute = index % 2 === 0 ? '00' : '30';
582
-
583
- return `${String(hour).padStart(2, '0')}:${minute}`;
584
- });
585
-
586
- function formatDateRangeLabel(
587
- start: string | undefined,
588
- end: string | undefined,
589
- getSettingValue: (k: string) => any,
590
- locale: string
591
- ) {
592
- if (start && end)
593
- return `${formatDateLocalized(start, getSettingValue, locale)} – ${formatDateLocalized(end, getSettingValue, locale)}`;
594
- if (start) return formatDateLocalized(start, getSettingValue, locale);
595
- return '';
596
- }
597
-
598
- function buildSessionRecurrencePayload(values: TurmaForm) {
599
- if (values.sessionRecurrenceMode === 'none') {
600
- return undefined;
601
- }
602
-
603
- const frequency =
604
- values.sessionRecurrenceMode === 'custom'
605
- ? values.sessionRecurrenceCustomFrequency
606
- : values.sessionRecurrenceMode === 'weekdays'
607
- ? 'weekly'
608
- : values.sessionRecurrenceMode;
609
-
610
- const daysOfWeek =
611
- values.sessionRecurrenceMode === 'weekdays'
612
- ? (['MO', 'TU', 'WE', 'TH', 'FR'] as SessionRecurrenceDay[])
613
- : frequency === 'weekly'
614
- ? values.sessionRecurrenceDaysOfWeek
615
- : undefined;
616
-
617
- return {
618
- frequency,
619
- interval: values.sessionRecurrenceInterval,
620
- until: values.sessionRecurrenceUntil!,
621
- ...(daysOfWeek?.length ? { daysOfWeek } : {}),
622
- };
623
- }
624
-
625
- function inferRecurrenceMode(
626
- summary?: SessionRecurrenceSummary | null
627
- ): SessionRecurrenceMode {
628
- if (!summary?.isRecurring || !summary.frequency) {
629
- return 'none';
630
- }
631
-
632
- if (
633
- summary.frequency === 'weekly' &&
634
- Array.isArray(summary.daysOfWeek) &&
635
- summary.daysOfWeek.join(',') === 'MO,TU,WE,TH,FR' &&
636
- (summary.interval ?? 1) === 1
637
- ) {
638
- return 'weekdays';
639
- }
640
-
641
- if ((summary.interval ?? 1) !== 1) {
642
- return 'custom';
643
- }
644
-
645
- return summary.frequency;
646
- }
647
-
648
- function getSuggestedEndTime(startTime?: string) {
649
- if (!startTime) return '';
650
-
651
- const startIndex = TIME_OPTIONS.indexOf(startTime);
652
- if (startIndex === -1) return '';
653
-
654
- return TIME_OPTIONS[Math.min(startIndex + 1, TIME_OPTIONS.length - 1)] ?? '';
655
- }
656
-
657
- function createClassCodeSeed() {
658
- return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
659
- .replace(/[^a-z0-9]/gi, '')
660
- .toUpperCase();
661
- }
662
-
663
- function buildClassCode(courseTitle?: string, seed?: string) {
664
- const normalizedPrefix = (courseTitle ?? '')
665
- .normalize('NFD')
666
- .replace(/[\u0300-\u036f]/g, '')
667
- .replace(/[^a-z0-9]/gi, '')
668
- .toUpperCase()
669
- .slice(0, 4);
670
-
671
- const prefix = normalizedPrefix || 'TURM';
672
- const codeSeed = seed || createClassCodeSeed();
673
-
674
- return `${prefix}-${codeSeed}`;
675
- }
676
280
 
677
281
  // ── Animations ────────────────────────────────────────────────────────────────
678
282
 
@@ -690,26 +294,15 @@ const stagger = {
690
294
 
691
295
  export default function TurmasPage() {
692
296
  const t = useTranslations('lms.ClassesPage');
693
- const courseSheetT = useTranslations('lms.CoursesPage');
694
297
  const router = useRouter();
695
298
  const { request, currentLocaleCode, getSettingValue, locales } = useApp();
696
299
 
697
- const [sheetOpen, setSheetOpen] = useState(false);
698
- const [editingTurma, setEditingTurma] = useState<Turma | null>(null);
300
+ const [classFormSheetOpen, setClassFormSheetOpen] = useState(false);
301
+ const [classFormClassId, setClassFormClassId] = useState<string | undefined>(
302
+ undefined
303
+ );
699
304
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
700
305
  const [turmaToDelete, setTurmaToDelete] = useState<Turma | null>(null);
701
- const [saving, setSaving] = useState(false);
702
- const [savingCourse, setSavingCourse] = useState(false);
703
- const [dateRangeOpen, setDateRangeOpen] = useState(false);
704
- const [dateRangeDraft, setDateRangeDraft] = useState<DateRange | undefined>();
705
- const [createCodeSeed, setCreateCodeSeed] = useState('');
706
- const [courseSheetOpen, setCourseSheetOpen] = useState(false);
707
- const [customRecurrenceDialogOpen, setCustomRecurrenceDialogOpen] =
708
- useState(false);
709
- const [previousRecurrenceMode, setPreviousRecurrenceMode] =
710
- useState<SessionRecurrenceMode>('none');
711
- const [createProfessorDialogOpen, setCreateProfessorDialogOpen] =
712
- useState(false);
713
306
 
714
307
  // Search inputs (uncommitted)
715
308
  const [buscaInput, setBuscaInput] = useState('');
@@ -731,99 +324,21 @@ export default function TurmasPage() {
731
324
  allowedValues: PAGE_SIZES,
732
325
  });
733
326
 
734
- // Double-click tracking
735
- const clickTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(
736
- new Map()
737
- );
738
- const customRecurrenceConfirmedRef = useRef(false);
739
-
740
- const form = useForm<TurmaForm>({
741
- resolver: zodResolver(getTurmaSchema(t)),
742
- defaultValues: {
743
- codigo: '',
744
- curso: '',
745
- courseId: undefined,
746
- instructorId: undefined,
747
- tipo: 'online',
748
- professor: '',
749
- vagas: 30,
750
- dataInicio: '',
751
- dataFim: '',
752
- horarioInicio: '',
753
- horarioFim: '',
754
- sessionRecurrenceMode: 'none',
755
- sessionRecurrenceCustomFrequency: 'weekly',
756
- sessionRecurrenceInterval: 1,
757
- sessionRecurrenceDaysOfWeek: [],
758
- sessionRecurrenceUntil: '',
759
- sessionTitleMode: 'default-course-code',
760
- sessionTitle: '',
761
- status: 'aberta',
327
+ const { data: coursesResponse } = useQuery<ApiCourseList>({
328
+ queryKey: ['lms-courses-for-class-form'],
329
+ queryFn: async () => {
330
+ const response = await request<ApiCourseList>({
331
+ url: '/lms/courses',
332
+ method: 'GET',
333
+ params: {
334
+ page: 1,
335
+ pageSize: 500,
336
+ offeringTypes: 'scheduled,blended',
337
+ },
338
+ });
339
+ return response.data;
762
340
  },
763
341
  });
764
- const courseForm = useForm<CourseSheetFormValues>({
765
- resolver: zodResolver(getCourseSheetSchema(courseSheetT)),
766
- defaultValues: DEFAULT_COURSE_FORM_VALUES,
767
- });
768
-
769
- const watchedFormValues = useWatch({ control: form.control });
770
-
771
- const { data: coursesResponse, refetch: refetchCourseOptions } =
772
- useQuery<ApiCourseList>({
773
- queryKey: ['lms-courses-for-class-form'],
774
- queryFn: async () => {
775
- const response = await request<ApiCourseList>({
776
- url: '/lms/courses',
777
- method: 'GET',
778
- params: {
779
- page: 1,
780
- pageSize: 500,
781
- offeringTypes: 'scheduled,blended',
782
- },
783
- });
784
- return response.data;
785
- },
786
- });
787
-
788
- const { data: categoryListData, refetch: refetchCategoryOptions } =
789
- useQuery<ApiCategoryList>({
790
- queryKey: ['category-options'],
791
- queryFn: async () => {
792
- const response = await request<ApiCategoryList>({
793
- url: '/category',
794
- method: 'GET',
795
- params: {
796
- page: 1,
797
- pageSize: 500,
798
- status: 'all',
799
- },
800
- });
801
-
802
- const payload = response.data as ApiCategoryList | ApiCategory[];
803
- if (Array.isArray(payload)) {
804
- return {
805
- data: payload,
806
- total: payload.length,
807
- page: 1,
808
- pageSize: payload.length,
809
- };
810
- }
811
-
812
- return payload;
813
- },
814
- initialData: {
815
- data: [],
816
- total: 0,
817
- page: 1,
818
- pageSize: 500,
819
- },
820
- });
821
-
822
- useEffect(() => {
823
- if (courseSheetOpen) {
824
- void refetchCategoryOptions();
825
- }
826
- }, [courseSheetOpen, refetchCategoryOptions]);
827
342
 
828
343
  const {
829
344
  data: classesResponse,
@@ -894,28 +409,6 @@ export default function TurmasPage() {
894
409
  return () => clearTimeout(timeoutId);
895
410
  }, [buscaInput]);
896
411
 
897
- useEffect(() => {
898
- setCurrentPage(1);
899
- }, [buscaDebounced, filtroStatusInput, filtroTipoInput, filtroCursoInput]);
900
-
901
- useEffect(() => {
902
- if (!sheetOpen || editingTurma || !createCodeSeed) return;
903
- if (form.getFieldState('codigo').isDirty) return;
904
-
905
- const nextCode = buildClassCode(
906
- watchedFormValues.curso || undefined,
907
- createCodeSeed
908
- );
909
-
910
- if (form.getValues('codigo') === nextCode) return;
911
-
912
- form.setValue('codigo', nextCode, {
913
- shouldDirty: false,
914
- shouldTouch: false,
915
- shouldValidate: true,
916
- });
917
- }, [createCodeSeed, editingTurma, form, sheetOpen, watchedFormValues.curso]);
918
-
919
412
  const courseOptions = useMemo(
920
413
  () =>
921
414
  (coursesResponse?.data ?? []).map((item) => ({
@@ -930,224 +423,6 @@ export default function TurmasPage() {
930
423
  [courseOptions]
931
424
  );
932
425
 
933
- const selectedCourse = useMemo(
934
- () =>
935
- courseOptions.find((item) => item.id === watchedFormValues.courseId) ??
936
- null,
937
- [courseOptions, watchedFormValues.courseId]
938
- );
939
-
940
- const selectedCourseTitle = selectedCourse?.title ?? watchedFormValues.curso;
941
- const defaultSessionTitle = useMemo(
942
- () => getDefaultSessionTitle(selectedCourseTitle, watchedFormValues.codigo),
943
- [selectedCourseTitle, watchedFormValues.codigo]
944
- );
945
- const filteredEndTimeOptions = useMemo(() => {
946
- const startTime = watchedFormValues.horarioInicio ?? '';
947
-
948
- if (!startTime) {
949
- return TIME_OPTIONS;
950
- }
951
-
952
- return TIME_OPTIONS.filter((time) => time >= startTime);
953
- }, [watchedFormValues.horarioInicio]);
954
-
955
- const categoryOptions = useMemo<CourseCategoryOption[]>(
956
- () =>
957
- (categoryListData?.data ?? [])
958
- .filter((category) => !!category.slug)
959
- .map((category) => ({
960
- value: category.slug,
961
- label: category.name || category.slug,
962
- }))
963
- .sort((a, b) => a.label.localeCompare(b.label)),
964
- [categoryListData]
965
- );
966
-
967
- const recurrenceDayOptions = useMemo(
968
- () =>
969
- (
970
- [
971
- ['MO', t('form.recurrence.customDialog.days.MO')],
972
- ['TU', t('form.recurrence.customDialog.days.TU')],
973
- ['WE', t('form.recurrence.customDialog.days.WE')],
974
- ['TH', t('form.recurrence.customDialog.days.TH')],
975
- ['FR', t('form.recurrence.customDialog.days.FR')],
976
- ['SA', t('form.recurrence.customDialog.days.SA')],
977
- ['SU', t('form.recurrence.customDialog.days.SU')],
978
- ] as const
979
- ).map(([value, label]) => ({
980
- value: value as SessionRecurrenceDay,
981
- label,
982
- })),
983
- [t]
984
- );
985
-
986
- const recurrenceSummaryText = useMemo(() => {
987
- const until = watchedFormValues.dataFim
988
- ? formatDateLocalized(
989
- watchedFormValues.dataFim,
990
- getSettingValue,
991
- currentLocaleCode
992
- )
993
- : '--';
994
-
995
- return t(
996
- `form.recurrence.summary.${watchedFormValues.sessionRecurrenceMode}`,
997
- {
998
- until,
999
- }
1000
- );
1001
- }, [
1002
- t,
1003
- watchedFormValues.sessionRecurrenceMode,
1004
- watchedFormValues.dataFim,
1005
- getSettingValue,
1006
- currentLocaleCode,
1007
- ]);
1008
-
1009
- useEffect(() => {
1010
- if (
1011
- !selectedCourseTitle ||
1012
- form.getValues('curso') === selectedCourseTitle
1013
- ) {
1014
- return;
1015
- }
1016
-
1017
- form.setValue('curso', selectedCourseTitle, {
1018
- shouldDirty: false,
1019
- shouldTouch: false,
1020
- shouldValidate: false,
1021
- });
1022
- }, [form, selectedCourseTitle]);
1023
-
1024
- useEffect(() => {
1025
- if (!dateRangeOpen) return;
1026
-
1027
- setDateRangeDraft({
1028
- from: watchedFormValues.dataInicio
1029
- ? new Date(`${watchedFormValues.dataInicio}T00:00:00`)
1030
- : undefined,
1031
- to: watchedFormValues.dataFim
1032
- ? new Date(`${watchedFormValues.dataFim}T00:00:00`)
1033
- : undefined,
1034
- });
1035
- }, [dateRangeOpen, watchedFormValues.dataFim, watchedFormValues.dataInicio]);
1036
-
1037
- useEffect(() => {
1038
- if (
1039
- watchedFormValues.sessionTitleMode !== 'default-course-code' ||
1040
- form.getValues('sessionTitle') === defaultSessionTitle
1041
- ) {
1042
- return;
1043
- }
1044
-
1045
- form.setValue('sessionTitle', defaultSessionTitle, {
1046
- shouldDirty: false,
1047
- shouldTouch: false,
1048
- shouldValidate: false,
1049
- });
1050
- }, [defaultSessionTitle, form, watchedFormValues.sessionTitleMode]);
1051
-
1052
- const customRecurrenceFrequency =
1053
- watchedFormValues.sessionRecurrenceMode === 'custom'
1054
- ? watchedFormValues.sessionRecurrenceCustomFrequency
1055
- : watchedFormValues.sessionRecurrenceMode === 'weekdays'
1056
- ? 'weekly'
1057
- : watchedFormValues.sessionRecurrenceMode === 'none'
1058
- ? watchedFormValues.sessionRecurrenceCustomFrequency
1059
- : watchedFormValues.sessionRecurrenceMode;
1060
-
1061
- const customRecurrenceNeedsWeekdays =
1062
- watchedFormValues.sessionRecurrenceMode === 'custom' &&
1063
- customRecurrenceFrequency === 'weekly';
1064
-
1065
- const weeklyNeedsDays = watchedFormValues.sessionRecurrenceMode === 'weekly';
1066
-
1067
- useEffect(() => {
1068
- if (!watchedFormValues.dataFim) {
1069
- return;
1070
- }
1071
-
1072
- form.setValue('sessionRecurrenceUntil', watchedFormValues.dataFim, {
1073
- shouldDirty: false,
1074
- shouldTouch: false,
1075
- shouldValidate: false,
1076
- });
1077
- }, [form, watchedFormValues.dataFim]);
1078
-
1079
- useEffect(() => {
1080
- if (!watchedFormValues.dataInicio) {
1081
- return;
1082
- }
1083
-
1084
- const defaultDay = getDayCodeFromDate(watchedFormValues.dataInicio);
1085
- const recurrenceMode = watchedFormValues.sessionRecurrenceMode;
1086
- const recurrenceDays = form.getValues('sessionRecurrenceDaysOfWeek') ?? [];
1087
-
1088
- if (recurrenceMode === 'weekly' && recurrenceDays.length === 0) {
1089
- form.setValue('sessionRecurrenceDaysOfWeek', [defaultDay], {
1090
- shouldDirty: false,
1091
- shouldTouch: false,
1092
- shouldValidate: false,
1093
- });
1094
- }
1095
-
1096
- if (recurrenceMode === 'weekdays') {
1097
- const weekdaySet = ['MO', 'TU', 'WE', 'TH', 'FR'];
1098
- const alreadySet =
1099
- recurrenceDays.length === weekdaySet.length &&
1100
- weekdaySet.every((d) =>
1101
- recurrenceDays.includes(d as SessionRecurrenceDay)
1102
- );
1103
- if (!alreadySet) {
1104
- form.setValue(
1105
- 'sessionRecurrenceDaysOfWeek',
1106
- ['MO', 'TU', 'WE', 'TH', 'FR'],
1107
- {
1108
- shouldDirty: false,
1109
- shouldTouch: false,
1110
- shouldValidate: false,
1111
- }
1112
- );
1113
- }
1114
- }
1115
- }, [
1116
- form,
1117
- watchedFormValues.dataInicio,
1118
- watchedFormValues.sessionRecurrenceMode,
1119
- ]);
1120
-
1121
- useEffect(() => {
1122
- const startTime = watchedFormValues.horarioInicio;
1123
- const endTime = watchedFormValues.horarioFim;
1124
-
1125
- if (!startTime) {
1126
- return;
1127
- }
1128
-
1129
- if (!endTime) {
1130
- const suggestedEndTime = getSuggestedEndTime(startTime);
1131
- if (suggestedEndTime) {
1132
- form.setValue('horarioFim', suggestedEndTime, {
1133
- shouldDirty: true,
1134
- shouldTouch: false,
1135
- shouldValidate: true,
1136
- });
1137
- }
1138
- return;
1139
- }
1140
-
1141
- if (endTime < startTime) {
1142
- const suggestedEndTime = getSuggestedEndTime(startTime);
1143
- form.setValue('horarioFim', suggestedEndTime || startTime, {
1144
- shouldDirty: true,
1145
- shouldTouch: true,
1146
- shouldValidate: true,
1147
- });
1148
- }
1149
- }, [form, watchedFormValues.horarioFim, watchedFormValues.horarioInicio]);
1150
-
1151
426
  const professorNameById = useMemo<Map<number, string>>(() => {
1152
427
  const map = new Map<number, string>();
1153
428
  for (const turma of classesResponse?.data ?? []) {
@@ -1185,40 +460,9 @@ export default function TurmasPage() {
1185
460
  }),
1186
461
  [classesResponse, professorNameById, courseLogoMap]
1187
462
  );
1188
- const previewTurma = useMemo<Turma | null>(() => {
1189
- if (!sheetOpen || !editingTurma) return null;
1190
-
1191
- return {
1192
- ...editingTurma,
1193
- codigo: watchedFormValues.codigo ?? editingTurma.codigo,
1194
- curso: selectedCourseTitle || editingTurma.curso,
1195
- cursoId: watchedFormValues.courseId ?? editingTurma.cursoId,
1196
- primaryColor: editingTurma.primaryColor ?? null,
1197
- tipo:
1198
- (watchedFormValues.tipo as Turma['tipo'] | undefined) ??
1199
- editingTurma.tipo,
1200
- professor: watchedFormValues.professor ?? editingTurma.professor,
1201
- vagas: watchedFormValues.vagas ?? editingTurma.vagas,
1202
- dataInicio: watchedFormValues.dataInicio ?? editingTurma.dataInicio,
1203
- dataFim: watchedFormValues.dataFim ?? editingTurma.dataFim,
1204
- horarioInicio:
1205
- watchedFormValues.horarioInicio ?? editingTurma.horarioInicio,
1206
- horarioFim: watchedFormValues.horarioFim ?? editingTurma.horarioFim,
1207
- status:
1208
- (watchedFormValues.status as Turma['status'] | undefined) ??
1209
- editingTurma.status,
1210
- };
1211
- }, [editingTurma, selectedCourseTitle, sheetOpen, watchedFormValues]);
1212
- const visibleTurmas = useMemo<Turma[]>(() => {
1213
- if (!previewTurma) return turmas;
1214
-
1215
- return turmas.map((turma) =>
1216
- turma.id === previewTurma.id ? previewTurma : turma
1217
- );
1218
- }, [previewTurma, turmas]);
463
+ const visibleTurmas = turmas;
1219
464
 
1220
465
  const totalItems = classesResponse?.total ?? 0;
1221
- const totalPages = Math.max(classesResponse?.lastPage ?? 1, 1);
1222
466
  const loading = isClassesLoading && !classesResponse;
1223
467
  const isRefreshing = isClassesFetching && !isClassesLoading;
1224
468
  const refetchersRef = useRef({
@@ -1233,12 +477,6 @@ export default function TurmasPage() {
1233
477
  };
1234
478
  }, [refetchClasses, refetchStats]);
1235
479
 
1236
- useEffect(() => {
1237
- if (currentPage > totalPages) {
1238
- setCurrentPage(totalPages);
1239
- }
1240
- }, [currentPage, totalPages]);
1241
-
1242
480
  useEffect(() => {
1243
481
  const refreshClassesData = () => {
1244
482
  void refetchersRef.current.refetchClasses();
@@ -1299,347 +537,17 @@ export default function TurmasPage() {
1299
537
  setDeleteDialogOpen(true);
1300
538
  }
1301
539
 
1302
- // ── Double-click ──────────────────────────────────────────────────────────
1303
-
1304
- function handleCardClick(turma: Turma) {
1305
- const existing = clickTimers.current.get(turma.id);
1306
- if (existing) {
1307
- clearTimeout(existing);
1308
- clickTimers.current.delete(turma.id);
1309
- router.push(`/lms/classes/${turma.id}`);
1310
- } else {
1311
- const t = setTimeout(() => clickTimers.current.delete(turma.id), 300);
1312
- clickTimers.current.set(turma.id, t);
1313
- }
1314
- }
1315
-
1316
540
  // ── CRUD ──────────────────────────────────────────────────────────────────
1317
541
 
1318
542
  function openCreateSheet() {
1319
- const nextSeed = createClassCodeSeed();
1320
- const defaultTitle = getDefaultSessionTitle(
1321
- undefined,
1322
- buildClassCode(undefined, nextSeed)
1323
- );
1324
-
1325
- setCreateCodeSeed(nextSeed);
1326
- setEditingTurma(null);
1327
- setPreviousRecurrenceMode('none');
1328
- setDateRangeOpen(false);
1329
- setDateRangeDraft(undefined);
1330
- form.reset({
1331
- codigo: buildClassCode(undefined, nextSeed),
1332
- curso: '',
1333
- courseId: undefined,
1334
- instructorId: undefined,
1335
- tipo: 'online',
1336
- professor: '',
1337
- vagas: 30,
1338
- dataInicio: '',
1339
- dataFim: '',
1340
- horarioInicio: '',
1341
- horarioFim: '',
1342
- sessionRecurrenceMode: 'none',
1343
- sessionRecurrenceCustomFrequency: 'weekly',
1344
- sessionRecurrenceInterval: 1,
1345
- sessionRecurrenceDaysOfWeek: [],
1346
- sessionRecurrenceUntil: '',
1347
- sessionTitleMode: 'default-course-code',
1348
- sessionTitle: defaultTitle,
1349
- status: 'aberta',
1350
- });
1351
- setSheetOpen(true);
543
+ setClassFormClassId(undefined);
544
+ setClassFormSheetOpen(true);
1352
545
  }
1353
546
 
1354
- async function openEditSheet(turma: Turma, e?: React.MouseEvent) {
547
+ function openEditSheet(turma: Turma, e?: React.MouseEvent) {
1355
548
  e?.stopPropagation();
1356
- setCreateCodeSeed('');
1357
- const response = await request<ApiClass>({
1358
- url: `/lms/classes/${turma.id}`,
1359
- method: 'GET',
1360
- });
1361
- const detailedTurma = mapApiClass(response.data);
1362
- const recurrenceSummary = response.data.sessionRecurrenceSummary;
1363
- const recurrenceMode = inferRecurrenceMode(recurrenceSummary);
1364
- const defaultDay = getDayCodeFromDate(detailedTurma.dataInicio);
1365
-
1366
- setEditingTurma(detailedTurma);
1367
- setPreviousRecurrenceMode(recurrenceMode);
1368
- setDateRangeOpen(false);
1369
- setDateRangeDraft({
1370
- from: detailedTurma.dataInicio
1371
- ? new Date(`${detailedTurma.dataInicio}T00:00:00`)
1372
- : undefined,
1373
- to: detailedTurma.dataFim
1374
- ? new Date(`${detailedTurma.dataFim}T00:00:00`)
1375
- : undefined,
1376
- });
1377
- form.reset({
1378
- codigo: detailedTurma.codigo,
1379
- curso: detailedTurma.curso,
1380
- courseId: detailedTurma.cursoId,
1381
- instructorId: detailedTurma.instructorId ?? undefined,
1382
- tipo: detailedTurma.tipo,
1383
- professor: detailedTurma.professor,
1384
- vagas: detailedTurma.vagas,
1385
- dataInicio: detailedTurma.dataInicio,
1386
- dataFim: detailedTurma.dataFim,
1387
- horarioInicio: detailedTurma.horarioInicio,
1388
- horarioFim: detailedTurma.horarioFim,
1389
- sessionRecurrenceMode: recurrenceMode,
1390
- sessionRecurrenceCustomFrequency:
1391
- recurrenceSummary?.frequency ?? 'weekly',
1392
- sessionRecurrenceInterval: recurrenceSummary?.interval ?? 1,
1393
- sessionRecurrenceDaysOfWeek:
1394
- recurrenceSummary?.daysOfWeek ??
1395
- (recurrenceMode === 'weekly' ? [defaultDay] : []),
1396
- sessionRecurrenceUntil: detailedTurma.dataFim,
1397
- sessionTitleMode:
1398
- response.data.sessionTitle &&
1399
- response.data.sessionTitle !==
1400
- getDefaultSessionTitle(detailedTurma.curso, detailedTurma.codigo)
1401
- ? 'custom'
1402
- : 'default-course-code',
1403
- sessionTitle:
1404
- response.data.sessionTitle ??
1405
- getDefaultSessionTitle(detailedTurma.curso, detailedTurma.codigo),
1406
- status: detailedTurma.status,
1407
- });
1408
- setSheetOpen(true);
1409
- }
1410
-
1411
- function handleRecurrenceModeChange(value: SessionRecurrenceMode) {
1412
- if (value === 'custom') {
1413
- setPreviousRecurrenceMode(
1414
- watchedFormValues.sessionRecurrenceMode ?? 'none'
1415
- );
1416
- form.setValue('sessionRecurrenceMode', 'custom', {
1417
- shouldDirty: true,
1418
- shouldTouch: true,
1419
- shouldValidate: true,
1420
- });
1421
- setCustomRecurrenceDialogOpen(true);
1422
- return;
1423
- }
1424
-
1425
- form.setValue('sessionRecurrenceMode', value, {
1426
- shouldDirty: true,
1427
- shouldTouch: true,
1428
- shouldValidate: true,
1429
- });
1430
-
1431
- if (value === 'weekdays') {
1432
- form.setValue('sessionRecurrenceCustomFrequency', 'weekly', {
1433
- shouldDirty: true,
1434
- shouldTouch: false,
1435
- shouldValidate: false,
1436
- });
1437
- form.setValue('sessionRecurrenceInterval', 1, {
1438
- shouldDirty: true,
1439
- shouldTouch: false,
1440
- shouldValidate: false,
1441
- });
1442
- form.setValue(
1443
- 'sessionRecurrenceDaysOfWeek',
1444
- ['MO', 'TU', 'WE', 'TH', 'FR'],
1445
- {
1446
- shouldDirty: true,
1447
- shouldTouch: false,
1448
- shouldValidate: true,
1449
- }
1450
- );
1451
- }
1452
-
1453
- if (value === 'weekly' && watchedFormValues.dataInicio) {
1454
- form.setValue(
1455
- 'sessionRecurrenceDaysOfWeek',
1456
- [getDayCodeFromDate(watchedFormValues.dataInicio)],
1457
- {
1458
- shouldDirty: true,
1459
- shouldTouch: false,
1460
- shouldValidate: true,
1461
- }
1462
- );
1463
- }
1464
-
1465
- setPreviousRecurrenceMode(value);
1466
- }
1467
-
1468
- function toggleCustomRecurrenceDay(day: SessionRecurrenceDay) {
1469
- const currentDays = watchedFormValues.sessionRecurrenceDaysOfWeek ?? [];
1470
- const nextDays = currentDays.includes(day)
1471
- ? currentDays.filter((item) => item !== day)
1472
- : [...currentDays, day];
1473
-
1474
- form.setValue('sessionRecurrenceDaysOfWeek', nextDays, {
1475
- shouldDirty: true,
1476
- shouldTouch: true,
1477
- shouldValidate: true,
1478
- });
1479
- }
1480
-
1481
- function handleCustomRecurrenceCancel() {
1482
- form.setValue('sessionRecurrenceMode', previousRecurrenceMode, {
1483
- shouldDirty: true,
1484
- shouldTouch: false,
1485
- shouldValidate: true,
1486
- });
1487
- setCustomRecurrenceDialogOpen(false);
1488
- }
1489
-
1490
- async function handleCustomRecurrenceConfirm() {
1491
- const valid = await form.trigger([
1492
- 'sessionRecurrenceInterval',
1493
- 'sessionRecurrenceDaysOfWeek',
1494
- 'dataInicio',
1495
- ]);
1496
-
1497
- if (!valid) {
1498
- return;
1499
- }
1500
-
1501
- setPreviousRecurrenceMode('custom');
1502
- customRecurrenceConfirmedRef.current = true;
1503
- form.setValue('sessionRecurrenceMode', 'custom', {
1504
- shouldDirty: true,
1505
- shouldTouch: true,
1506
- shouldValidate: true,
1507
- });
1508
- setCustomRecurrenceDialogOpen(false);
1509
- }
1510
-
1511
- function openCourseCreateSheet() {
1512
- courseForm.reset(DEFAULT_COURSE_FORM_VALUES);
1513
- setCourseSheetOpen(true);
1514
- }
1515
-
1516
- async function onSubmitCourse(data: CourseSheetFormValues) {
1517
- setSavingCourse(true);
1518
-
1519
- try {
1520
- const payload = {
1521
- name: data.nomeInterno.trim(),
1522
- slug: data.slug.trim().toLowerCase(),
1523
- title: data.tituloComercial,
1524
- description: data.descricao,
1525
- level: toApiCourseLevel(data.nivel),
1526
- status: toApiCourseStatus(data.status),
1527
- categorySlugs: data.categorias,
1528
- primaryColor: data.primaryColor,
1529
- primaryContrastColor: getContrastColor(data.primaryColor),
1530
- secondaryColor: data.secondaryColor,
1531
- secondaryContrastColor: getContrastColor(data.secondaryColor),
1532
- };
1533
-
1534
- const response = await request<ApiCreatedCourse>({
1535
- url: '/lms/courses',
1536
- method: 'POST',
1537
- data: payload,
1538
- });
1539
-
1540
- const createdCourse = response.data;
1541
- await refetchCourseOptions();
1542
- form.setValue('courseId', createdCourse.id, {
1543
- shouldDirty: true,
1544
- shouldTouch: true,
1545
- shouldValidate: true,
1546
- });
1547
- form.setValue('curso', createdCourse.title, {
1548
- shouldDirty: true,
1549
- shouldTouch: true,
1550
- shouldValidate: false,
1551
- });
1552
- setCourseSheetOpen(false);
1553
- toast.success(courseSheetT('toasts.courseCreated'));
1554
- } catch {
1555
- toast.error(t('messages.classCreateCourseError'));
1556
- } finally {
1557
- setSavingCourse(false);
1558
- }
1559
- }
1560
-
1561
- async function onSubmit(data: TurmaForm) {
1562
- setSaving(true);
1563
- const selectedCourse = courseOptions.find(
1564
- (item) => item.id === data.courseId
1565
- );
1566
- const instructorId = data.instructorId ?? form.getValues('instructorId');
1567
- const sessionTitle =
1568
- data.sessionTitleMode === 'custom'
1569
- ? data.sessionTitle?.trim() || defaultSessionTitle
1570
- : defaultSessionTitle;
1571
- const sessionTemplate = {
1572
- title: sessionTitle,
1573
- recurrence: buildSessionRecurrencePayload(data),
1574
- };
1575
-
1576
- if (!selectedCourse) {
1577
- toast.error(t('form.validation.cursoRequired'));
1578
- setSaving(false);
1579
- return;
1580
- }
1581
-
1582
- try {
1583
- if (editingTurma) {
1584
- await request({
1585
- url: `/lms/classes/${editingTurma.id}`,
1586
- method: 'PATCH',
1587
- data: {
1588
- code: data.codigo,
1589
- title: `${selectedCourse.title} - ${data.codigo}`,
1590
- courseId: selectedCourse.id,
1591
- instructorId: instructorId,
1592
- deliveryMode: toApiType(data.tipo),
1593
- status: toApiStatus(data.status),
1594
- startDate: data.dataInicio,
1595
- endDate: data.dataFim || null,
1596
- startTime: data.horarioInicio,
1597
- endTime: data.horarioFim,
1598
- capacity: data.vagas,
1599
- sessionTemplate,
1600
- },
1601
- });
1602
-
1603
- toast.success(t('toasts.turmaUpdated'));
1604
- } else {
1605
- const response = await request<ApiClass>({
1606
- url: '/lms/classes',
1607
- method: 'POST',
1608
- data: {
1609
- code: data.codigo,
1610
- title: `${selectedCourse.title} - ${data.codigo}`,
1611
- courseId: selectedCourse.id,
1612
- instructorId: instructorId,
1613
- deliveryMode: toApiType(data.tipo),
1614
- status: toApiStatus(data.status),
1615
- startDate: data.dataInicio,
1616
- endDate: data.dataFim || null,
1617
- startTime: data.horarioInicio,
1618
- endTime: data.horarioFim,
1619
- capacity: data.vagas,
1620
- sessionTemplate,
1621
- },
1622
- });
1623
-
1624
- toast.success(t('toasts.turmaCreated'));
1625
- await refetchClasses();
1626
- await refetchStats();
1627
- notifyLmsDashboardUpdated();
1628
- setSaving(false);
1629
- setSheetOpen(false);
1630
-
1631
- return;
1632
- }
1633
-
1634
- await refetchClasses();
1635
- await refetchStats();
1636
- notifyLmsDashboardUpdated();
1637
- setSheetOpen(false);
1638
- } catch {
1639
- toast.error(t('messages.classSaveError'));
1640
- } finally {
1641
- setSaving(false);
1642
- }
549
+ setClassFormClassId(String(turma.id));
550
+ setClassFormSheetOpen(true);
1643
551
  }
1644
552
 
1645
553
  async function confirmDelete() {
@@ -1661,26 +569,6 @@ export default function TurmasPage() {
1661
569
  }
1662
570
  }
1663
571
 
1664
- const handleProfessorCreated = async (instructor: {
1665
- id: number;
1666
- personId: number;
1667
- name: string;
1668
- qualificationSlugs: string[];
1669
- }) => {
1670
- form.setValue('instructorId', instructor.id, {
1671
- shouldDirty: true,
1672
- shouldTouch: true,
1673
- shouldValidate: true,
1674
- });
1675
- form.setValue('professor', instructor.name, {
1676
- shouldDirty: true,
1677
- shouldTouch: true,
1678
- shouldValidate: true,
1679
- });
1680
-
1681
- await refetchClasses();
1682
- };
1683
-
1684
572
  // ── KPIs ────────────────────────────────────────────────���─────────────────
1685
573
 
1686
574
  const kpis: KpiCardItem[] = [
@@ -1802,15 +690,24 @@ export default function TurmasPage() {
1802
690
  <div className="space-y-1">
1803
691
  <SearchBar
1804
692
  searchQuery={buscaInput}
1805
- onSearchChange={setBuscaInput}
1806
- onSearch={() => setBuscaDebounced(buscaInput.trim())}
693
+ onSearchChange={(value) => {
694
+ setBuscaInput(value);
695
+ setCurrentPage(1);
696
+ }}
697
+ onSearch={() => {
698
+ setBuscaDebounced(buscaInput.trim());
699
+ setCurrentPage(1);
700
+ }}
1807
701
  placeholder={t('filters.searchPlaceholder')}
1808
702
  controls={[
1809
703
  {
1810
704
  id: 'status-filter',
1811
705
  type: 'select',
1812
706
  value: filtroStatusInput,
1813
- onChange: setFiltroStatusInput,
707
+ onChange: (value) => {
708
+ setFiltroStatusInput(value);
709
+ setCurrentPage(1);
710
+ },
1814
711
  placeholder: t('filters.status'),
1815
712
  options: [
1816
713
  { value: 'todos', label: t('filters.allStatuses') },
@@ -1824,7 +721,10 @@ export default function TurmasPage() {
1824
721
  id: 'type-filter',
1825
722
  type: 'select',
1826
723
  value: filtroTipoInput,
1827
- onChange: setFiltroTipoInput,
724
+ onChange: (value) => {
725
+ setFiltroTipoInput(value);
726
+ setCurrentPage(1);
727
+ },
1828
728
  placeholder: t('filters.type'),
1829
729
  options: [
1830
730
  { value: 'todos', label: t('filters.allTypes') },
@@ -1837,7 +737,10 @@ export default function TurmasPage() {
1837
737
  id: 'course-filter',
1838
738
  type: 'select',
1839
739
  value: filtroCursoInput,
1840
- onChange: setFiltroCursoInput,
740
+ onChange: (value) => {
741
+ setFiltroCursoInput(value);
742
+ setCurrentPage(1);
743
+ },
1841
744
  placeholder: t('filters.course'),
1842
745
  options: [
1843
746
  { value: 'todos', label: t('filters.allCourses') },
@@ -1973,7 +876,9 @@ export default function TurmasPage() {
1973
876
  <motion.div key={turma.id} variants={fadeUp}>
1974
877
  <Card
1975
878
  className="group relative cursor-pointer overflow-hidden border-border/70 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md"
1976
- onClick={() => handleCardClick(turma)}
879
+ onDoubleClick={() =>
880
+ router.push(`/lms/classes/${turma.id}`)
881
+ }
1977
882
  title={t('cards.tooltip')}
1978
883
  >
1979
884
  <div
@@ -1984,13 +889,13 @@ export default function TurmasPage() {
1984
889
  : undefined
1985
890
  }
1986
891
  />
1987
- <CardContent className="p-5">
1988
- <div className="mb-4 flex items-start gap-3">
892
+ <CardContent className="px-3 py-2">
893
+ <div className="mb-2 flex items-start gap-2">
1989
894
  <CourseAvatar
1990
895
  fileId={turma.logoFileId}
1991
896
  title={turma.curso}
1992
- className="size-12 rounded-xl"
1993
- iconSize="size-6"
897
+ className="size-9 rounded-lg"
898
+ iconSize="size-5"
1994
899
  />
1995
900
  <div className="min-w-0 flex-1">
1996
901
  <div className="mb-1 flex items-start justify-between gap-2">
@@ -2036,59 +941,68 @@ export default function TurmasPage() {
2036
941
  </DropdownMenuContent>
2037
942
  </DropdownMenu>
2038
943
  </div>
2039
- <p className="text-xs text-muted-foreground">
944
+ <div className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
2040
945
  <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
2041
946
  {turma.codigo}
2042
947
  </code>
2043
- <span className="mx-1.5 text-muted-foreground/50">
2044
- |
2045
- </span>
948
+ <span className="text-muted-foreground/50">|</span>
949
+ {turma.professorAvatarId ? (
950
+ <img
951
+ src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${turma.professorAvatarId}`}
952
+ alt={turma.professor}
953
+ className="size-4 rounded-full object-cover"
954
+ onError={(e) => {
955
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
956
+ }}
957
+ />
958
+ ) : (
959
+ <span className="inline-flex size-4 items-center justify-center rounded-full bg-muted text-[8px] font-semibold uppercase text-muted-foreground">
960
+ {turma.professor
961
+ .split(' ')
962
+ .slice(0, 2)
963
+ .map((p) => p[0])
964
+ .join('')}
965
+ </span>
966
+ )}
2046
967
  <span>{turma.professor}</span>
2047
- </p>
968
+ </div>
2048
969
  </div>
2049
970
  </div>
2050
971
 
2051
- <div className="mb-4 flex flex-wrap items-center gap-1.5">
972
+ <div className="mb-2 flex flex-wrap items-center gap-1">
2052
973
  <Badge
2053
- variant={STATUS_VARIANT[turma.status]}
2054
- className="text-[11px]"
974
+ variant="outline"
975
+ className={`text-[11px] ${STATUS_CLASS[turma.status]}`}
2055
976
  >
2056
977
  {t(`status.${turma.status}`)}
2057
978
  </Badge>
2058
- <span className="inline-flex items-center gap-1 rounded-full border bg-muted px-2.5 py-0.5 text-[11px] font-medium text-foreground">
979
+ <span
980
+ className={`inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-[11px] font-medium ${TIPO_CLASS[turma.tipo]}`}
981
+ >
2059
982
  <TipoIcon className="size-3" />
2060
983
  {t(`type.${turma.tipo}`)}
2061
984
  </span>
2062
985
  </div>
2063
986
 
2064
- <div className="mb-4 rounded-lg bg-muted/40 p-3">
2065
- <div className="mb-2 flex items-center justify-between text-sm">
987
+ <div className="mb-2 space-y-1">
988
+ <div className="flex items-center justify-between text-[11px]">
2066
989
  <span className="text-muted-foreground">
2067
990
  {t('cards.occupancy')}
2068
991
  </span>
2069
- <span className="font-semibold">
2070
- {turma.matriculados}/{turma.vagas}
992
+ <span className="font-semibold text-foreground">
993
+ {turma.matriculados}/{turma.vagas} · {ocupacao}%
2071
994
  </span>
2072
995
  </div>
2073
- <div className="relative h-2 w-full overflow-hidden rounded-full bg-muted">
996
+ <div className="relative h-1 w-full overflow-hidden rounded-full bg-muted">
2074
997
  <div
2075
998
  className="h-full rounded-full bg-primary transition-all duration-500"
2076
999
  style={{ width: `${Math.min(ocupacao, 100)}%` }}
2077
1000
  />
2078
1001
  </div>
2079
- <div className="mt-1.5 flex justify-between text-[11px]">
2080
- <span className="text-muted-foreground">
2081
- {ocupacao}% {t('cards.occupied')}
2082
- </span>
2083
- <span className="font-medium text-foreground">
2084
- {Math.max(turma.vagas - turma.matriculados, 0)}{' '}
2085
- {t('cards.freeVacancies')}
2086
- </span>
2087
- </div>
2088
1002
  </div>
2089
1003
 
2090
- <div className="mb-4 grid grid-cols-2 gap-2">
2091
- <div className="rounded-lg border bg-background p-2.5">
1004
+ <div className="grid grid-cols-2 gap-1.5">
1005
+ <div className="rounded-lg border bg-background p-2">
2092
1006
  <div className="mb-1 flex items-center gap-1.5 text-[11px] text-muted-foreground">
2093
1007
  <CalendarIcon className="size-3" />{' '}
2094
1008
  {t('cards.period')}
@@ -2109,25 +1023,13 @@ export default function TurmasPage() {
2109
1023
  )}
2110
1024
  </p>
2111
1025
  </div>
2112
- <div className="rounded-lg border bg-background p-2.5">
1026
+ <div className="rounded-lg border bg-background p-2">
2113
1027
  <div className="mb-1 flex items-center gap-1.5 text-[11px] text-muted-foreground">
2114
1028
  <Clock className="size-3" /> {t('cards.schedule')}
2115
1029
  </div>
2116
1030
  <p className="text-xs font-medium">{scheduleLabel}</p>
2117
1031
  </div>
2118
1032
  </div>
2119
-
2120
- <div className="flex items-center justify-between rounded-lg bg-muted/40 px-3 py-2.5">
2121
- <div className="flex items-center gap-1.5">
2122
- <Users className="size-4 text-muted-foreground" />
2123
- <span className="text-sm font-medium">
2124
- {turma.matriculados}
2125
- </span>
2126
- <span className="text-xs text-muted-foreground">
2127
- {t('cards.enrolled')}
2128
- </span>
2129
- </div>
2130
- </div>
2131
1033
  </CardContent>
2132
1034
  </Card>
2133
1035
  </motion.div>
@@ -2166,7 +1068,9 @@ export default function TurmasPage() {
2166
1068
  <TableRow
2167
1069
  key={turma.id}
2168
1070
  className="cursor-pointer"
2169
- onClick={() => handleCardClick(turma)}
1071
+ onDoubleClick={() =>
1072
+ router.push(`/lms/classes/${turma.id}`)
1073
+ }
2170
1074
  title={t('cards.tooltip')}
2171
1075
  >
2172
1076
  <TableCell>
@@ -2195,14 +1099,16 @@ export default function TurmasPage() {
2195
1099
  </TableCell>
2196
1100
  <TableCell>
2197
1101
  <Badge
2198
- variant={STATUS_VARIANT[turma.status]}
2199
- className="text-[11px]"
1102
+ variant="outline"
1103
+ className={`text-[11px] ${STATUS_CLASS[turma.status]}`}
2200
1104
  >
2201
1105
  {t(`status.${turma.status}`)}
2202
1106
  </Badge>
2203
1107
  </TableCell>
2204
1108
  <TableCell>
2205
- <span className="inline-flex items-center gap-1 rounded-full border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground">
1109
+ <span
1110
+ className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium ${TIPO_CLASS[turma.tipo]}`}
1111
+ >
2206
1112
  <TipoIcon className="size-3" />
2207
1113
  {t(`type.${turma.tipo}`)}
2208
1114
  </span>
@@ -2289,900 +1195,15 @@ export default function TurmasPage() {
2289
1195
  )}
2290
1196
  </div>
2291
1197
 
2292
- {/* Sheet */}
2293
- <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
2294
- <SheetContent
2295
- side="right"
2296
- className="flex w-full flex-col overflow-y-auto sm:max-w-2xl"
2297
- >
2298
- <SheetHeader className="shrink-0">
2299
- <SheetTitle>
2300
- {editingTurma ? t('form.title.edit') : t('form.title.create')}
2301
- </SheetTitle>
2302
- <SheetDescription>{t('form.description')}</SheetDescription>
2303
- </SheetHeader>
2304
- <form
2305
- onSubmit={form.handleSubmit(onSubmit)}
2306
- className="flex flex-1 flex-col gap-5 px-4 py-6"
2307
- >
2308
- <div className="grid gap-4 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
2309
- <Field>
2310
- <FieldLabel htmlFor="codigo">
2311
- {t('form.fields.code.label')}
2312
- </FieldLabel>
2313
- <Input
2314
- id="codigo"
2315
- value={watchedFormValues.codigo ?? ''}
2316
- className="uppercase"
2317
- onChange={(event) =>
2318
- form.setValue('codigo', event.target.value.toUpperCase(), {
2319
- shouldDirty: true,
2320
- shouldTouch: true,
2321
- shouldValidate: true,
2322
- })
2323
- }
2324
- />
2325
- <FieldDescription>
2326
- Codigo gerado automaticamente, mas pode ser editado.
2327
- </FieldDescription>
2328
- <FieldError>{form.formState.errors.codigo?.message}</FieldError>
2329
- </Field>
2330
-
2331
- <Field>
2332
- <FieldLabel>
2333
- {t('form.fields.course.label')}{' '}
2334
- <span className="text-destructive">*</span>
2335
- </FieldLabel>
2336
- <div className="flex items-end gap-2">
2337
- <div className="min-w-0 flex-1">
2338
- <EntityPicker<
2339
- {
2340
- id: number;
2341
- title: string;
2342
- code?: string;
2343
- logoFileId?: number | null;
2344
- },
2345
- TurmaForm
2346
- >
2347
- form={form}
2348
- name="courseId"
2349
- valueType="number"
2350
- placeholder={t('form.fields.course.placeholder')}
2351
- entityLabel={t('form.fields.course.label')}
2352
- initialSelectedLabel={selectedCourseTitle}
2353
- searchPlaceholder={t('form.fields.course.placeholder')}
2354
- emptyStateDescription={t(
2355
- 'components.entityPicker.courses.empty'
2356
- )}
2357
- loadingLabel={t(
2358
- 'components.entityPicker.courses.loading'
2359
- )}
2360
- noResultsLabel={t(
2361
- 'components.entityPicker.courses.empty'
2362
- )}
2363
- showCreateButton={false}
2364
- renderOption={({ option }) => (
2365
- <div className="flex items-center gap-2 py-0.5">
2366
- <CourseAvatar
2367
- fileId={option.logoFileId}
2368
- title={option.title}
2369
- className="size-8 shrink-0 rounded-lg"
2370
- iconSize="size-4"
2371
- />
2372
- <div className="min-w-0">
2373
- <p className="truncate text-sm">{option.title}</p>
2374
- <p className="truncate text-xs text-muted-foreground">
2375
- {option.code ?? '—'} · #{option.id}
2376
- </p>
2377
- </div>
2378
- </div>
2379
- )}
2380
- renderSelectedValue={({ option }) => (
2381
- <div className="flex items-center gap-2">
2382
- <CourseAvatar
2383
- fileId={option?.logoFileId}
2384
- title={option?.title ?? ''}
2385
- className="size-5 shrink-0 rounded"
2386
- iconSize="size-3"
2387
- />
2388
- <span className="truncate">{option?.title}</span>
2389
- </div>
2390
- )}
2391
- onChange={(value, option) => {
2392
- const courseId =
2393
- typeof value === 'number' ? value : undefined;
2394
- const courseTitle =
2395
- option && typeof option.title === 'string'
2396
- ? option.title
2397
- : '';
2398
-
2399
- form.setValue('courseId', courseId, {
2400
- shouldDirty: true,
2401
- shouldTouch: true,
2402
- shouldValidate: true,
2403
- });
2404
- form.setValue('curso', courseTitle, {
2405
- shouldDirty: true,
2406
- shouldTouch: true,
2407
- shouldValidate: false,
2408
- });
2409
- }}
2410
- loadOptions={async ({ page, pageSize, search }) => {
2411
- const response = await request<ApiCourseList>({
2412
- url: '/lms/courses',
2413
- method: 'GET',
2414
- params: {
2415
- page,
2416
- pageSize,
2417
- offeringTypes: 'scheduled,blended',
2418
- ...(search.trim() ? { search: search.trim() } : {}),
2419
- },
2420
- });
2421
-
2422
- return {
2423
- items: response.data?.data ?? [],
2424
- hasMore: page < (response.data?.lastPage ?? 1),
2425
- };
2426
- }}
2427
- getOptionValue={(option) => option.id}
2428
- getOptionLabel={(option) => option.title}
2429
- />
2430
- </div>
2431
-
2432
- <Button
2433
- type="button"
2434
- variant="outline"
2435
- size="icon"
2436
- className="shrink-0"
2437
- onClick={openCourseCreateSheet}
2438
- aria-label={courseSheetT('actions.createCourse')}
2439
- >
2440
- <Plus className="h-4 w-4" />
2441
- </Button>
2442
- </div>
2443
- </Field>
2444
- </div>
2445
-
2446
- <div className="rounded-lg border border-border/70 p-4">
2447
- <div className="grid gap-4">
2448
- <Field>
2449
- <FieldLabel>
2450
- {t('form.fields.startDate.label')} /{' '}
2451
- {t('form.fields.endDate.label')}{' '}
2452
- <span className="text-destructive">*</span>
2453
- </FieldLabel>
2454
- <Popover open={dateRangeOpen} onOpenChange={setDateRangeOpen}>
2455
- <PopoverTrigger asChild>
2456
- <Button
2457
- type="button"
2458
- variant="outline"
2459
- className={`w-full justify-start text-left font-normal ${
2460
- !watchedFormValues.dataInicio ||
2461
- !watchedFormValues.dataFim
2462
- ? 'text-muted-foreground'
2463
- : ''
2464
- }`}
2465
- >
2466
- <CalendarIcon className="mr-2 size-4" />
2467
- {formatDateRangeLabel(
2468
- watchedFormValues.dataInicio,
2469
- watchedFormValues.dataFim,
2470
- getSettingValue,
2471
- currentLocaleCode
2472
- ) || t('form.fields.startDate.placeholder')}
2473
- </Button>
2474
- </PopoverTrigger>
2475
- <PopoverContent className="w-auto p-0" align="start">
2476
- <Calendar
2477
- mode="range"
2478
- numberOfMonths={2}
2479
- selected={dateRangeDraft}
2480
- onSelect={(range) => {
2481
- setDateRangeDraft(range);
2482
-
2483
- if (!range?.from || !range?.to) {
2484
- form.setValue('dataInicio', '', {
2485
- shouldDirty: true,
2486
- shouldTouch: true,
2487
- shouldValidate: true,
2488
- });
2489
- form.setValue('dataFim', '', {
2490
- shouldDirty: true,
2491
- shouldTouch: true,
2492
- shouldValidate: true,
2493
- });
2494
- return;
2495
- }
2496
-
2497
- form.setValue(
2498
- 'dataInicio',
2499
- format(range.from, 'yyyy-MM-dd'),
2500
- {
2501
- shouldDirty: true,
2502
- shouldTouch: true,
2503
- shouldValidate: true,
2504
- }
2505
- );
2506
- form.setValue(
2507
- 'dataFim',
2508
- format(range.to, 'yyyy-MM-dd'),
2509
- {
2510
- shouldDirty: true,
2511
- shouldTouch: true,
2512
- shouldValidate: true,
2513
- }
2514
- );
2515
- }}
2516
- initialFocus
2517
- />
2518
- </PopoverContent>
2519
- </Popover>
2520
- <FieldError>
2521
- {form.formState.errors.dataInicio?.message ||
2522
- form.formState.errors.dataFim?.message}
2523
- </FieldError>
2524
- </Field>
2525
-
2526
- <div className="grid gap-4 md:grid-cols-2">
2527
- <Field>
2528
- <FieldLabel htmlFor="horarioInicio">
2529
- {t('form.fields.startTime.label')}{' '}
2530
- <span className="text-destructive">*</span>
2531
- </FieldLabel>
2532
- <Controller
2533
- name="horarioInicio"
2534
- control={form.control}
2535
- render={({ field }) => (
2536
- <Select
2537
- onValueChange={(value) => {
2538
- field.onChange(value);
2539
- }}
2540
- value={field.value}
2541
- >
2542
- <SelectTrigger id="horarioInicio">
2543
- <SelectValue
2544
- placeholder={t(
2545
- 'form.fields.startTime.placeholder'
2546
- )}
2547
- />
2548
- </SelectTrigger>
2549
- <SelectContent>
2550
- {TIME_OPTIONS.map((time) => (
2551
- <SelectItem key={time} value={time}>
2552
- {time}
2553
- </SelectItem>
2554
- ))}
2555
- </SelectContent>
2556
- </Select>
2557
- )}
2558
- />
2559
- <FieldDescription>
2560
- Escolha o horario de inicio na lista para preencher mais
2561
- rapido.
2562
- </FieldDescription>
2563
- <FieldError>
2564
- {form.formState.errors.horarioInicio?.message}
2565
- </FieldError>
2566
- </Field>
2567
-
2568
- <Field>
2569
- <FieldLabel htmlFor="horarioFim">
2570
- {t('form.fields.endTime.label')}{' '}
2571
- <span className="text-destructive">*</span>
2572
- </FieldLabel>
2573
- <Controller
2574
- name="horarioFim"
2575
- control={form.control}
2576
- render={({ field }) => (
2577
- <Select
2578
- onValueChange={field.onChange}
2579
- value={field.value}
2580
- >
2581
- <SelectTrigger id="horarioFim">
2582
- <SelectValue
2583
- placeholder={t('form.fields.endTime.placeholder')}
2584
- />
2585
- </SelectTrigger>
2586
- <SelectContent>
2587
- {filteredEndTimeOptions.map((time) => (
2588
- <SelectItem key={time} value={time}>
2589
- {time}
2590
- </SelectItem>
2591
- ))}
2592
- </SelectContent>
2593
- </Select>
2594
- )}
2595
- />
2596
- <FieldDescription>
2597
- O termino mostra apenas horarios iguais ou depois do
2598
- inicio.
2599
- </FieldDescription>
2600
- <FieldError>
2601
- {form.formState.errors.horarioFim?.message}
2602
- </FieldError>
2603
- </Field>
2604
- </div>
2605
- </div>
2606
- </div>
2607
-
2608
- <div className="rounded-lg border border-border/70 p-4">
2609
- <div className="space-y-4">
2610
- <div className="space-y-1">
2611
- <h3 className="text-sm font-semibold">
2612
- {t('form.recurrence.sectionTitle')}
2613
- </h3>
2614
- <p className="text-sm text-muted-foreground">
2615
- {t('form.recurrence.sectionDescription')}
2616
- </p>
2617
- </div>
2618
-
2619
- <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
2620
- <Field>
2621
- <FieldLabel>{t('form.recurrence.label')}</FieldLabel>
2622
- <Select
2623
- value={watchedFormValues.sessionRecurrenceMode}
2624
- onValueChange={(value) =>
2625
- handleRecurrenceModeChange(
2626
- value as SessionRecurrenceMode
2627
- )
2628
- }
2629
- >
2630
- <SelectTrigger>
2631
- <SelectValue />
2632
- </SelectTrigger>
2633
- <SelectContent>
2634
- <SelectItem value="none">
2635
- {t('form.recurrence.options.none')}
2636
- </SelectItem>
2637
- <SelectItem value="daily">
2638
- {t('form.recurrence.options.daily')}
2639
- </SelectItem>
2640
- <SelectItem value="weekly">
2641
- {t('form.recurrence.options.weekly')}
2642
- </SelectItem>
2643
- <SelectItem value="monthly">
2644
- {t('form.recurrence.options.monthly')}
2645
- </SelectItem>
2646
- <SelectItem value="yearly">
2647
- {t('form.recurrence.options.yearly')}
2648
- </SelectItem>
2649
- <SelectItem value="weekdays">
2650
- {t('form.recurrence.options.weekdays')}
2651
- </SelectItem>
2652
- <SelectItem value="custom">
2653
- {t('form.recurrence.options.custom')}
2654
- </SelectItem>
2655
- </SelectContent>
2656
- </Select>
2657
- <FieldDescription>{recurrenceSummaryText}</FieldDescription>
2658
- <FieldError>
2659
- {form.formState.errors.sessionRecurrenceMode?.message}
2660
- </FieldError>
2661
- </Field>
2662
- </div>
2663
-
2664
- {weeklyNeedsDays && (
2665
- <Field>
2666
- <FieldLabel>
2667
- {t('form.recurrence.customDialog.repeatOn')}
2668
- </FieldLabel>
2669
- <div className="grid grid-cols-4 gap-2 sm:grid-cols-7">
2670
- {recurrenceDayOptions.map((day) => {
2671
- const active = (
2672
- watchedFormValues.sessionRecurrenceDaysOfWeek ?? []
2673
- ).includes(day.value);
2674
-
2675
- return (
2676
- <Button
2677
- key={day.value}
2678
- type="button"
2679
- variant={active ? 'default' : 'outline'}
2680
- size="sm"
2681
- className="h-9"
2682
- onClick={() => toggleCustomRecurrenceDay(day.value)}
2683
- >
2684
- {day.label}
2685
- </Button>
2686
- );
2687
- })}
2688
- </div>
2689
- <FieldError>
2690
- {
2691
- form.formState.errors.sessionRecurrenceDaysOfWeek
2692
- ?.message
2693
- }
2694
- </FieldError>
2695
- </Field>
2696
- )}
2697
-
2698
- <div className="grid gap-4 md:grid-cols-[minmax(0,0.55fr)_minmax(0,1fr)]">
2699
- <Field>
2700
- <FieldLabel>
2701
- {t('form.recurrence.titleMode.label')}
2702
- </FieldLabel>
2703
- <Controller
2704
- name="sessionTitleMode"
2705
- control={form.control}
2706
- render={({ field }) => (
2707
- <Select
2708
- value={field.value}
2709
- onValueChange={(value) => {
2710
- field.onChange(value);
2711
- if (value === 'default-course-code') {
2712
- form.setValue(
2713
- 'sessionTitle',
2714
- defaultSessionTitle,
2715
- {
2716
- shouldDirty: true,
2717
- shouldTouch: false,
2718
- shouldValidate: false,
2719
- }
2720
- );
2721
- }
2722
- }}
2723
- >
2724
- <SelectTrigger>
2725
- <SelectValue />
2726
- </SelectTrigger>
2727
- <SelectContent>
2728
- <SelectItem value="default-course-code">
2729
- {t('form.recurrence.titleMode.default')}
2730
- </SelectItem>
2731
- <SelectItem value="custom">
2732
- {t('form.recurrence.titleMode.custom')}
2733
- </SelectItem>
2734
- </SelectContent>
2735
- </Select>
2736
- )}
2737
- />
2738
- </Field>
2739
-
2740
- <Field>
2741
- <FieldLabel htmlFor="sessionTitle">
2742
- {t('form.fields.sessionTitle.label')}
2743
- </FieldLabel>
2744
- <Input
2745
- id="sessionTitle"
2746
- value={
2747
- watchedFormValues.sessionTitleMode ===
2748
- 'default-course-code'
2749
- ? defaultSessionTitle
2750
- : (watchedFormValues.sessionTitle ?? '')
2751
- }
2752
- placeholder={t('form.fields.sessionTitle.placeholder')}
2753
- disabled={
2754
- watchedFormValues.sessionTitleMode ===
2755
- 'default-course-code'
2756
- }
2757
- onChange={(event) =>
2758
- form.setValue('sessionTitle', event.target.value, {
2759
- shouldDirty: true,
2760
- shouldTouch: true,
2761
- shouldValidate: false,
2762
- })
2763
- }
2764
- />
2765
- <FieldDescription>
2766
- {watchedFormValues.sessionTitleMode ===
2767
- 'default-course-code'
2768
- ? defaultSessionTitle ||
2769
- t('form.fields.sessionTitle.placeholder')
2770
- : recurrenceSummaryText}
2771
- </FieldDescription>
2772
- <FieldError>
2773
- {form.formState.errors.sessionTitle?.message}
2774
- </FieldError>
2775
- </Field>
2776
- </div>
2777
- </div>
2778
- </div>
2779
-
2780
- <div className="grid gap-4 md:grid-cols-3">
2781
- <Field>
2782
- <FieldLabel>
2783
- {t('form.fields.type.label')}{' '}
2784
- <span className="text-destructive">*</span>
2785
- </FieldLabel>
2786
- <Controller
2787
- name="tipo"
2788
- control={form.control}
2789
- render={({ field }) => (
2790
- <Select onValueChange={field.onChange} value={field.value}>
2791
- <SelectTrigger>
2792
- <SelectValue />
2793
- </SelectTrigger>
2794
- <SelectContent>
2795
- <SelectItem value="online">
2796
- {t('type.online')}
2797
- </SelectItem>
2798
- <SelectItem value="presencial">
2799
- {t('type.inPerson')}
2800
- </SelectItem>
2801
- <SelectItem value="hibrida">
2802
- {t('type.hybrid')}
2803
- </SelectItem>
2804
- </SelectContent>
2805
- </Select>
2806
- )}
2807
- />
2808
- <FieldError>{form.formState.errors.tipo?.message}</FieldError>
2809
- </Field>
2810
-
2811
- <Field>
2812
- <FieldLabel>
2813
- {t('form.fields.status.label')}{' '}
2814
- <span className="text-destructive">*</span>
2815
- </FieldLabel>
2816
- <Controller
2817
- name="status"
2818
- control={form.control}
2819
- render={({ field }) => (
2820
- <Select onValueChange={field.onChange} value={field.value}>
2821
- <SelectTrigger>
2822
- <SelectValue />
2823
- </SelectTrigger>
2824
- <SelectContent>
2825
- <SelectItem value="aberta">
2826
- {t('status.open')}
2827
- </SelectItem>
2828
- <SelectItem value="em_andamento">
2829
- {t('status.inProgress')}
2830
- </SelectItem>
2831
- <SelectItem value="concluida">
2832
- {t('status.completed')}
2833
- </SelectItem>
2834
- <SelectItem value="cancelada">
2835
- {t('status.cancelled')}
2836
- </SelectItem>
2837
- </SelectContent>
2838
- </Select>
2839
- )}
2840
- />
2841
- <FieldError>{form.formState.errors.status?.message}</FieldError>
2842
- </Field>
2843
-
2844
- <Field>
2845
- <FieldLabel htmlFor="vagas">
2846
- {t('form.fields.vacancies.label')}{' '}
2847
- <span className="text-destructive">*</span>
2848
- </FieldLabel>
2849
- <Input
2850
- id="vagas"
2851
- type="number"
2852
- min={1}
2853
- {...form.register('vagas')}
2854
- />
2855
- <FieldError>{form.formState.errors.vagas?.message}</FieldError>
2856
- </Field>
2857
- </div>
2858
-
2859
- <Field>
2860
- <FieldLabel>
2861
- {t('form.fields.professor.label')}{' '}
2862
- <span className="text-destructive">*</span>
2863
- </FieldLabel>
2864
- <div className="flex items-end gap-2">
2865
- <div className="flex-1">
2866
- <EntityPicker<InstructorOption, TurmaForm>
2867
- form={form}
2868
- name="instructorId"
2869
- valueType="number"
2870
- placeholder={t('form.fields.professor.placeholder')}
2871
- initialSelectedLabel={watchedFormValues.professor ?? ''}
2872
- searchPlaceholder={t('form.fields.professor.placeholder')}
2873
- emptyStateDescription={t(
2874
- 'form.fields.professor.emptyState'
2875
- )}
2876
- noResultsLabel={t('form.fields.professor.noResults')}
2877
- showCreateButton={false}
2878
- clearable={false}
2879
- getOptionValue={(opt) => opt.id}
2880
- getOptionLabel={(opt) => opt.name}
2881
- onChange={(value, option) => {
2882
- form.setValue(
2883
- 'instructorId',
2884
- value as number | undefined,
2885
- {
2886
- shouldDirty: true,
2887
- shouldTouch: true,
2888
- shouldValidate: true,
2889
- }
2890
- );
2891
- form.setValue('professor', option?.name ?? '', {
2892
- shouldDirty: true,
2893
- shouldTouch: true,
2894
- shouldValidate: true,
2895
- });
2896
- }}
2897
- loadOptions={async ({ page, pageSize, search }) => {
2898
- const response = await request<
2899
- | InstructorApiRow[]
2900
- | {
2901
- data?: InstructorApiRow[];
2902
- items?: InstructorApiRow[];
2903
- rows?: InstructorApiRow[];
2904
- total?: number;
2905
- lastPage?: number;
2906
- }
2907
- >({
2908
- url: '/lms/instructors',
2909
- method: 'GET',
2910
- params: {
2911
- page,
2912
- pageSize,
2913
- qualificationSlugs: ['class-sessions'],
2914
- ...(search.trim() ? { search: search.trim() } : {}),
2915
- },
2916
- });
2917
-
2918
- const payload = response.data;
2919
- const rows = Array.isArray(payload)
2920
- ? payload
2921
- : Array.isArray(payload?.data)
2922
- ? payload.data
2923
- : Array.isArray(payload?.items)
2924
- ? payload.items
2925
- : Array.isArray(payload?.rows)
2926
- ? payload.rows
2927
- : [];
2928
- const lastPage =
2929
- !Array.isArray(payload) && payload?.lastPage
2930
- ? payload.lastPage
2931
- : 1;
2932
-
2933
- const items = rows
2934
- .map(normalizeInstructorOption)
2935
- .filter((opt): opt is InstructorOption => opt !== null);
2936
-
2937
- return { items, hasMore: page < lastPage };
2938
- }}
2939
- renderOption={({ option }) => {
2940
- const initials = option.name
2941
- .split(' ')
2942
- .filter(Boolean)
2943
- .slice(0, 2)
2944
- .map((p) => p[0]?.toUpperCase() ?? '')
2945
- .join('');
2946
- const avatarUrl = getInstructorAvatarUrl(option);
2947
- return (
2948
- <div className="flex min-w-0 items-center gap-3 py-0.5">
2949
- <Avatar className="h-8 w-8 shrink-0 rounded-lg border border-border/60">
2950
- <AvatarImage src={avatarUrl} />
2951
- <AvatarFallback className="rounded-lg bg-muted text-[11px] font-semibold text-foreground">
2952
- {initials}
2953
- </AvatarFallback>
2954
- </Avatar>
2955
- <span className="truncate text-sm">
2956
- {option.name}
2957
- </span>
2958
- </div>
2959
- );
2960
- }}
2961
- renderSelectedValue={({ option, label }) => {
2962
- const name = option?.name ?? label;
2963
- const initials = name
2964
- .split(' ')
2965
- .filter(Boolean)
2966
- .slice(0, 2)
2967
- .map((p) => p[0]?.toUpperCase() ?? '')
2968
- .join('');
2969
- const avatarUrl = getInstructorAvatarUrl(option ?? undefined);
2970
- return (
2971
- <div className="flex items-center gap-2">
2972
- <Avatar className="h-5 w-5 shrink-0 rounded">
2973
- <AvatarImage src={avatarUrl} />
2974
- <AvatarFallback className="rounded bg-muted text-[10px] font-semibold">
2975
- {initials}
2976
- </AvatarFallback>
2977
- </Avatar>
2978
- <span className="truncate">{name}</span>
2979
- </div>
2980
- );
2981
- }}
2982
- />
2983
- </div>
2984
- <Button
2985
- type="button"
2986
- variant="outline"
2987
- size="icon"
2988
- className="shrink-0"
2989
- onClick={() => setCreateProfessorDialogOpen(true)}
2990
- aria-label={t('sheet.lessonForm.createInstructor')}
2991
- >
2992
- <Plus className="h-4 w-4" />
2993
- </Button>
2994
- </div>
2995
- <FieldError>
2996
- {form.formState.errors.professor?.message}
2997
- </FieldError>
2998
- </Field>
2999
-
3000
- <SheetFooter className="mt-auto shrink-0 gap-2 pt-4 px-0">
3001
- <Button type="submit" disabled={saving} className="gap-2">
3002
- {saving && <Loader2 className="size-4 animate-spin" />}
3003
- {editingTurma
3004
- ? t('form.actions.save')
3005
- : t('form.actions.create')}
3006
- </Button>
3007
- </SheetFooter>
3008
- </form>
3009
- </SheetContent>
3010
- </Sheet>
3011
-
3012
- <CourseFormSheet
3013
- key="inline-course-create"
3014
- open={courseSheetOpen}
3015
- onOpenChange={setCourseSheetOpen}
3016
- editing={false}
3017
- saving={savingCourse}
3018
- form={courseForm}
3019
- onSubmit={onSubmitCourse}
3020
- categories={categoryOptions}
3021
- onCreateCategory={() => router.push('/category?new=1')}
3022
- t={courseSheetT}
3023
- />
3024
-
3025
- <CreateLmsPersonSheet
3026
- open={createProfessorDialogOpen}
3027
- onOpenChange={setCreateProfessorDialogOpen}
3028
- onCreated={handleProfessorCreated}
3029
- title={t('sheet.lessonForm.createInstructorTitle')}
3030
- description={t('sheet.lessonForm.createInstructorDescription')}
3031
- submitLabel={t('sheet.lessonForm.createInstructorSubmit')}
3032
- successMessage={t('sheet.lessonForm.createInstructorSuccess')}
3033
- errorMessage={t('sheet.lessonForm.createInstructorError')}
3034
- defaultQualificationSlugs={['class-sessions']}
3035
- />
3036
-
3037
- <Dialog
3038
- open={customRecurrenceDialogOpen}
3039
- onOpenChange={(open) => {
3040
- if (!open) {
3041
- if (customRecurrenceConfirmedRef.current) {
3042
- customRecurrenceConfirmedRef.current = false;
3043
- setCustomRecurrenceDialogOpen(false);
3044
- return;
3045
- }
3046
-
3047
- handleCustomRecurrenceCancel();
3048
- return;
3049
- }
3050
-
3051
- setCustomRecurrenceDialogOpen(true);
1198
+ <ClassFormSheet
1199
+ open={classFormSheetOpen}
1200
+ onOpenChange={setClassFormSheetOpen}
1201
+ classId={classFormClassId}
1202
+ onSuccess={async () => {
1203
+ await Promise.all([refetchClasses(), refetchStats()]);
1204
+ notifyLmsDashboardUpdated();
3052
1205
  }}
3053
- >
3054
- <DialogContent className="sm:max-w-xl">
3055
- <DialogHeader>
3056
- <DialogTitle>{t('form.recurrence.customDialog.title')}</DialogTitle>
3057
- <DialogDescription>
3058
- {t('form.recurrence.customDialog.description')}
3059
- </DialogDescription>
3060
- </DialogHeader>
3061
-
3062
- <div className="grid gap-5 py-2">
3063
- <div className="grid gap-4 sm:grid-cols-[140px_minmax(0,1fr)] sm:items-end">
3064
- <Field>
3065
- <FieldLabel>
3066
- {t('form.recurrence.customDialog.repeatEvery')}
3067
- </FieldLabel>
3068
- <Input
3069
- type="number"
3070
- min={1}
3071
- value={watchedFormValues.sessionRecurrenceInterval ?? 1}
3072
- onChange={(event) =>
3073
- form.setValue(
3074
- 'sessionRecurrenceInterval',
3075
- Number(event.target.value) || 1,
3076
- {
3077
- shouldDirty: true,
3078
- shouldTouch: true,
3079
- shouldValidate: true,
3080
- }
3081
- )
3082
- }
3083
- />
3084
- <FieldError>
3085
- {form.formState.errors.sessionRecurrenceInterval?.message}
3086
- </FieldError>
3087
- </Field>
3088
-
3089
- <Field>
3090
- <FieldLabel>&nbsp;</FieldLabel>
3091
- <Controller
3092
- name="sessionRecurrenceCustomFrequency"
3093
- control={form.control}
3094
- render={({ field }) => (
3095
- <Select
3096
- value={field.value}
3097
- onValueChange={(value) => {
3098
- field.onChange(value);
3099
- if (
3100
- value === 'weekly' &&
3101
- (watchedFormValues.sessionRecurrenceDaysOfWeek ?? [])
3102
- .length === 0 &&
3103
- watchedFormValues.dataInicio
3104
- ) {
3105
- form.setValue(
3106
- 'sessionRecurrenceDaysOfWeek',
3107
- [getDayCodeFromDate(watchedFormValues.dataInicio)],
3108
- {
3109
- shouldDirty: true,
3110
- shouldTouch: false,
3111
- shouldValidate: true,
3112
- }
3113
- );
3114
- }
3115
- }}
3116
- >
3117
- <SelectTrigger>
3118
- <SelectValue />
3119
- </SelectTrigger>
3120
- <SelectContent>
3121
- <SelectItem value="daily">
3122
- {t('form.recurrence.customDialog.frequency.daily')}
3123
- </SelectItem>
3124
- <SelectItem value="weekly">
3125
- {t('form.recurrence.customDialog.frequency.weekly')}
3126
- </SelectItem>
3127
- <SelectItem value="monthly">
3128
- {t('form.recurrence.customDialog.frequency.monthly')}
3129
- </SelectItem>
3130
- <SelectItem value="yearly">
3131
- {t('form.recurrence.customDialog.frequency.yearly')}
3132
- </SelectItem>
3133
- </SelectContent>
3134
- </Select>
3135
- )}
3136
- />
3137
- </Field>
3138
- </div>
3139
-
3140
- {customRecurrenceNeedsWeekdays && (
3141
- <Field>
3142
- <FieldLabel>
3143
- {t('form.recurrence.customDialog.repeatOn')}
3144
- </FieldLabel>
3145
- <div className="grid grid-cols-4 gap-2 sm:grid-cols-7">
3146
- {recurrenceDayOptions.map((day) => {
3147
- const active = (
3148
- watchedFormValues.sessionRecurrenceDaysOfWeek ?? []
3149
- ).includes(day.value);
3150
-
3151
- return (
3152
- <Button
3153
- key={day.value}
3154
- type="button"
3155
- variant={active ? 'default' : 'outline'}
3156
- size="sm"
3157
- className="h-9"
3158
- onClick={() => toggleCustomRecurrenceDay(day.value)}
3159
- >
3160
- {day.label}
3161
- </Button>
3162
- );
3163
- })}
3164
- </div>
3165
- <FieldError>
3166
- {form.formState.errors.sessionRecurrenceDaysOfWeek?.message}
3167
- </FieldError>
3168
- </Field>
3169
- )}
3170
- </div>
3171
-
3172
- <DialogFooter className="gap-2">
3173
- <Button
3174
- type="button"
3175
- variant="outline"
3176
- onClick={handleCustomRecurrenceCancel}
3177
- >
3178
- {t('form.recurrence.customDialog.cancel')}
3179
- </Button>
3180
- <Button type="button" onClick={handleCustomRecurrenceConfirm}>
3181
- {t('form.recurrence.customDialog.confirm')}
3182
- </Button>
3183
- </DialogFooter>
3184
- </DialogContent>
3185
- </Dialog>
1206
+ />
3186
1207
 
3187
1208
  {/* Delete Dialog */}
3188
1209
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>