@hed-hog/lms 0.0.347 → 0.0.350

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 (414) 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 +22 -0
  58. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  59. package/dist/certificate/certificate.controller.js +12 -0
  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 +25 -2
  69. package/dist/certificate/certificate.service.d.ts.map +1 -1
  70. package/dist/certificate/certificate.service.js +87 -2
  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.controller.d.ts +3 -3
  77. package/dist/class-group/class-group.mcp-tools.d.ts +87 -0
  78. package/dist/class-group/class-group.mcp-tools.d.ts.map +1 -0
  79. package/dist/class-group/class-group.mcp-tools.js +553 -0
  80. package/dist/class-group/class-group.mcp-tools.js.map +1 -0
  81. package/dist/class-group/class-group.module.d.ts.map +1 -1
  82. package/dist/class-group/class-group.module.js +2 -1
  83. package/dist/class-group/class-group.module.js.map +1 -1
  84. package/dist/class-group/class-group.service.d.ts +6 -4
  85. package/dist/class-group/class-group.service.d.ts.map +1 -1
  86. package/dist/class-group/class-group.service.js +45 -2
  87. package/dist/class-group/class-group.service.js.map +1 -1
  88. package/dist/course/course-operations-integration.service.d.ts +40 -0
  89. package/dist/course/course-operations-integration.service.d.ts.map +1 -0
  90. package/dist/course/course-operations-integration.service.js +372 -0
  91. package/dist/course/course-operations-integration.service.js.map +1 -0
  92. package/dist/course/course-structure.controller.d.ts +43 -4
  93. package/dist/course/course-structure.controller.d.ts.map +1 -1
  94. package/dist/course/course-structure.controller.js +22 -0
  95. package/dist/course/course-structure.controller.js.map +1 -1
  96. package/dist/course/course-structure.service.d.ts +42 -1
  97. package/dist/course/course-structure.service.d.ts.map +1 -1
  98. package/dist/course/course-structure.service.js +199 -32
  99. package/dist/course/course-structure.service.js.map +1 -1
  100. package/dist/course/course.controller.d.ts +12 -0
  101. package/dist/course/course.controller.d.ts.map +1 -1
  102. package/dist/course/course.mcp-tools.d.ts +90 -0
  103. package/dist/course/course.mcp-tools.d.ts.map +1 -0
  104. package/dist/course/course.mcp-tools.js +520 -0
  105. package/dist/course/course.mcp-tools.js.map +1 -0
  106. package/dist/course/course.module.d.ts.map +1 -1
  107. package/dist/course/course.module.js +8 -1
  108. package/dist/course/course.module.js.map +1 -1
  109. package/dist/course/course.service.d.ts +15 -1
  110. package/dist/course/course.service.d.ts.map +1 -1
  111. package/dist/course/course.service.js +70 -35
  112. package/dist/course/course.service.js.map +1 -1
  113. package/dist/course/dto/create-course.dto.d.ts +1 -0
  114. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  115. package/dist/course/dto/create-course.dto.js +7 -0
  116. package/dist/course/dto/create-course.dto.js.map +1 -1
  117. package/dist/course/dto/update-course-resources.dto.d.ts +11 -0
  118. package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -0
  119. package/dist/course/dto/update-course-resources.dto.js +51 -0
  120. package/dist/course/dto/update-course-resources.dto.js.map +1 -0
  121. package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts +23 -0
  122. package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts.map +1 -0
  123. package/dist/course-lesson-discussion/course-lesson-discussion.controller.js +78 -0
  124. package/dist/course-lesson-discussion/course-lesson-discussion.controller.js.map +1 -0
  125. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts +22 -0
  126. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts.map +1 -0
  127. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js +120 -0
  128. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js.map +1 -0
  129. package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts +3 -0
  130. package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts.map +1 -0
  131. package/dist/course-lesson-discussion/course-lesson-discussion.module.js +26 -0
  132. package/dist/course-lesson-discussion/course-lesson-discussion.module.js.map +1 -0
  133. package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts +49 -0
  134. package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts.map +1 -0
  135. package/dist/course-lesson-discussion/course-lesson-discussion.service.js +272 -0
  136. package/dist/course-lesson-discussion/course-lesson-discussion.service.js.map +1 -0
  137. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts +6 -0
  138. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts.map +1 -0
  139. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js +33 -0
  140. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js.map +1 -0
  141. package/dist/course-lesson-note/course-lesson-note.controller.d.ts +53 -0
  142. package/dist/course-lesson-note/course-lesson-note.controller.d.ts.map +1 -0
  143. package/dist/course-lesson-note/course-lesson-note.controller.js +93 -0
  144. package/dist/course-lesson-note/course-lesson-note.controller.js.map +1 -0
  145. package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts +27 -0
  146. package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts.map +1 -0
  147. package/dist/course-lesson-note/course-lesson-note.mcp-tools.js +145 -0
  148. package/dist/course-lesson-note/course-lesson-note.mcp-tools.js.map +1 -0
  149. package/dist/course-lesson-note/course-lesson-note.module.d.ts +3 -0
  150. package/dist/course-lesson-note/course-lesson-note.module.d.ts.map +1 -0
  151. package/dist/course-lesson-note/course-lesson-note.module.js +26 -0
  152. package/dist/course-lesson-note/course-lesson-note.module.js.map +1 -0
  153. package/dist/course-lesson-note/course-lesson-note.service.d.ts +59 -0
  154. package/dist/course-lesson-note/course-lesson-note.service.d.ts.map +1 -0
  155. package/dist/course-lesson-note/course-lesson-note.service.js +195 -0
  156. package/dist/course-lesson-note/course-lesson-note.service.js.map +1 -0
  157. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts +6 -0
  158. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts.map +1 -0
  159. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js +33 -0
  160. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js.map +1 -0
  161. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts +6 -0
  162. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts.map +1 -0
  163. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js +35 -0
  164. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js.map +1 -0
  165. package/dist/dashboard/dashboard.mcp-tools.d.ts +10 -0
  166. package/dist/dashboard/dashboard.mcp-tools.d.ts.map +1 -0
  167. package/dist/dashboard/dashboard.mcp-tools.js +46 -0
  168. package/dist/dashboard/dashboard.mcp-tools.js.map +1 -0
  169. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  170. package/dist/dashboard/dashboard.module.js +2 -1
  171. package/dist/dashboard/dashboard.module.js.map +1 -1
  172. package/dist/enterprise/enterprise.mcp-tools.d.ts +82 -0
  173. package/dist/enterprise/enterprise.mcp-tools.d.ts.map +1 -0
  174. package/dist/enterprise/enterprise.mcp-tools.js +516 -0
  175. package/dist/enterprise/enterprise.mcp-tools.js.map +1 -0
  176. package/dist/enterprise/enterprise.module.d.ts.map +1 -1
  177. package/dist/enterprise/enterprise.module.js +2 -1
  178. package/dist/enterprise/enterprise.module.js.map +1 -1
  179. package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -1
  180. package/dist/enterprise/training/enterprise-training.module.js +11 -1
  181. package/dist/enterprise/training/enterprise-training.module.js.map +1 -1
  182. package/dist/enterprise/training/training-admin.controller.d.ts +2 -2
  183. package/dist/enterprise/training/training-admin.mcp-tools.d.ts +79 -0
  184. package/dist/enterprise/training/training-admin.mcp-tools.d.ts.map +1 -0
  185. package/dist/enterprise/training/training-admin.mcp-tools.js +620 -0
  186. package/dist/enterprise/training/training-admin.mcp-tools.js.map +1 -0
  187. package/dist/enterprise/training/training-admin.service.d.ts +2 -2
  188. package/dist/enterprise/training/training-instructor.mcp-tools.d.ts +47 -0
  189. package/dist/enterprise/training/training-instructor.mcp-tools.d.ts.map +1 -0
  190. package/dist/enterprise/training/training-instructor.mcp-tools.js +275 -0
  191. package/dist/enterprise/training/training-instructor.mcp-tools.js.map +1 -0
  192. package/dist/enterprise/training/training-student.controller.d.ts +24 -0
  193. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
  194. package/dist/enterprise/training/training-student.controller.js +22 -0
  195. package/dist/enterprise/training/training-student.controller.js.map +1 -1
  196. package/dist/enterprise/training/training-student.mcp-tools.d.ts +27 -0
  197. package/dist/enterprise/training/training-student.mcp-tools.d.ts.map +1 -0
  198. package/dist/enterprise/training/training-student.mcp-tools.js +186 -0
  199. package/dist/enterprise/training/training-student.mcp-tools.js.map +1 -0
  200. package/dist/enterprise/training/training-student.service.d.ts +32 -0
  201. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  202. package/dist/enterprise/training/training-student.service.js +138 -0
  203. package/dist/enterprise/training/training-student.service.js.map +1 -1
  204. package/dist/evaluation/evaluation.controller.d.ts +4 -4
  205. package/dist/evaluation/evaluation.mcp-tools.d.ts +25 -0
  206. package/dist/evaluation/evaluation.mcp-tools.d.ts.map +1 -0
  207. package/dist/evaluation/evaluation.mcp-tools.js +220 -0
  208. package/dist/evaluation/evaluation.mcp-tools.js.map +1 -0
  209. package/dist/evaluation/evaluation.module.d.ts.map +1 -1
  210. package/dist/evaluation/evaluation.module.js +2 -1
  211. package/dist/evaluation/evaluation.module.js.map +1 -1
  212. package/dist/evaluation/evaluation.service.d.ts +4 -4
  213. package/dist/exam/dto/create-exam-question.dto.d.ts +2 -0
  214. package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -1
  215. package/dist/exam/dto/create-exam-question.dto.js +10 -0
  216. package/dist/exam/dto/create-exam-question.dto.js.map +1 -1
  217. package/dist/exam/dto/create-exam.dto.d.ts +2 -0
  218. package/dist/exam/dto/create-exam.dto.d.ts.map +1 -1
  219. package/dist/exam/dto/create-exam.dto.js +10 -0
  220. package/dist/exam/dto/create-exam.dto.js.map +1 -1
  221. package/dist/exam/dto/create-question-subject.dto.d.ts +5 -0
  222. package/dist/exam/dto/create-question-subject.dto.d.ts.map +1 -0
  223. package/dist/exam/dto/create-question-subject.dto.js +28 -0
  224. package/dist/exam/dto/create-question-subject.dto.js.map +1 -0
  225. package/dist/exam/exam-attempt.controller.d.ts +4 -0
  226. package/dist/exam/exam-attempt.controller.d.ts.map +1 -1
  227. package/dist/exam/exam-attempt.service.d.ts +7 -1
  228. package/dist/exam/exam-attempt.service.d.ts.map +1 -1
  229. package/dist/exam/exam-attempt.service.js +47 -17
  230. package/dist/exam/exam-attempt.service.js.map +1 -1
  231. package/dist/exam/exam.controller.d.ts +34 -0
  232. package/dist/exam/exam.controller.d.ts.map +1 -1
  233. package/dist/exam/exam.controller.js +27 -0
  234. package/dist/exam/exam.controller.js.map +1 -1
  235. package/dist/exam/exam.mcp-tools.d.ts +62 -0
  236. package/dist/exam/exam.mcp-tools.d.ts.map +1 -0
  237. package/dist/exam/exam.mcp-tools.js +430 -0
  238. package/dist/exam/exam.mcp-tools.js.map +1 -0
  239. package/dist/exam/exam.module.d.ts.map +1 -1
  240. package/dist/exam/exam.module.js +2 -1
  241. package/dist/exam/exam.module.js.map +1 -1
  242. package/dist/exam/exam.service.d.ts +38 -0
  243. package/dist/exam/exam.service.d.ts.map +1 -1
  244. package/dist/exam/exam.service.js +114 -17
  245. package/dist/exam/exam.service.js.map +1 -1
  246. package/dist/index.d.ts +9 -0
  247. package/dist/index.d.ts.map +1 -1
  248. package/dist/index.js +9 -0
  249. package/dist/index.js.map +1 -1
  250. package/dist/instructor/instructor.mcp-tools.d.ts +41 -0
  251. package/dist/instructor/instructor.mcp-tools.d.ts.map +1 -0
  252. package/dist/instructor/instructor.mcp-tools.js +326 -0
  253. package/dist/instructor/instructor.mcp-tools.js.map +1 -0
  254. package/dist/instructor/instructor.module.d.ts.map +1 -1
  255. package/dist/instructor/instructor.module.js +2 -1
  256. package/dist/instructor/instructor.module.js.map +1 -1
  257. package/dist/lms.module.d.ts.map +1 -1
  258. package/dist/lms.module.js +15 -0
  259. package/dist/lms.module.js.map +1 -1
  260. package/dist/realtime/lms-realtime.controller.d.ts +7 -0
  261. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -0
  262. package/dist/realtime/lms-realtime.controller.js +34 -0
  263. package/dist/realtime/lms-realtime.controller.js.map +1 -0
  264. package/dist/realtime/lms-realtime.module.d.ts +3 -0
  265. package/dist/realtime/lms-realtime.module.d.ts.map +1 -0
  266. package/dist/realtime/lms-realtime.module.js +25 -0
  267. package/dist/realtime/lms-realtime.module.js.map +1 -0
  268. package/dist/realtime/lms-realtime.service.d.ts +36 -0
  269. package/dist/realtime/lms-realtime.service.d.ts.map +1 -0
  270. package/dist/realtime/lms-realtime.service.js +59 -0
  271. package/dist/realtime/lms-realtime.service.js.map +1 -0
  272. package/dist/realtime/lms-realtime.subscriber.d.ts +10 -0
  273. package/dist/realtime/lms-realtime.subscriber.d.ts.map +1 -0
  274. package/dist/realtime/lms-realtime.subscriber.js +70 -0
  275. package/dist/realtime/lms-realtime.subscriber.js.map +1 -0
  276. package/dist/reports/reports.mcp-tools.d.ts +10 -0
  277. package/dist/reports/reports.mcp-tools.d.ts.map +1 -0
  278. package/dist/reports/reports.mcp-tools.js +50 -0
  279. package/dist/reports/reports.mcp-tools.js.map +1 -0
  280. package/dist/reports/reports.module.d.ts.map +1 -1
  281. package/dist/reports/reports.module.js +2 -1
  282. package/dist/reports/reports.module.js.map +1 -1
  283. package/dist/training/training.mcp-tools.d.ts +20 -0
  284. package/dist/training/training.mcp-tools.d.ts.map +1 -0
  285. package/dist/training/training.mcp-tools.js +181 -0
  286. package/dist/training/training.mcp-tools.js.map +1 -0
  287. package/dist/training/training.module.d.ts.map +1 -1
  288. package/dist/training/training.module.js +2 -1
  289. package/dist/training/training.module.js.map +1 -1
  290. package/hedhog/data/integration_event_catalog.yaml +69 -0
  291. package/hedhog/data/menu.yaml +34 -0
  292. package/hedhog/data/route.yaml +2351 -0
  293. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +168 -103
  294. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +80 -1
  295. package/hedhog/frontend/app/_components/course-picker.tsx.ejs +228 -0
  296. package/hedhog/frontend/app/_lib/hooks/use-lms-realtime-refresh.ts.ejs +58 -0
  297. package/hedhog/frontend/app/achievements/page.tsx.ejs +844 -0
  298. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +1010 -0
  299. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +61 -2
  300. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +7 -0
  301. package/hedhog/frontend/app/classes/page.tsx.ejs +55 -2060
  302. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +28 -0
  303. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -0
  304. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +0 -9
  305. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +80 -66
  306. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +583 -8
  307. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +527 -57
  308. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -1
  309. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +106 -4
  310. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +2 -1
  311. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +11 -1
  312. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +53 -6
  313. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +13 -2
  314. package/hedhog/frontend/app/courses/page.tsx.ejs +175 -29
  315. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -0
  316. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +7 -0
  317. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +169 -2
  318. package/hedhog/frontend/app/exams/page.tsx.ejs +77 -22
  319. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +1 -0
  320. package/hedhog/frontend/app/instructors/page.tsx.ejs +1 -0
  321. package/hedhog/frontend/app/paths/page.tsx.ejs +6 -24
  322. package/hedhog/frontend/app/training/page.tsx.ejs +6 -24
  323. package/hedhog/frontend/messages/en.json +314 -12
  324. package/hedhog/frontend/messages/pt.json +314 -12
  325. package/hedhog/query/triggers.sql +53 -0
  326. package/hedhog/table/achievement.yaml +46 -0
  327. package/hedhog/table/bitcode_wallet.yaml +18 -0
  328. package/hedhog/table/bitcode_wallet_transaction.yaml +22 -0
  329. package/hedhog/table/certificate.yaml +3 -0
  330. package/hedhog/table/course.yaml +4 -0
  331. package/hedhog/table/course_file.yaml +23 -0
  332. package/hedhog/table/course_lesson.yaml +5 -0
  333. package/hedhog/table/course_lesson_discussion_like.yaml +21 -0
  334. package/hedhog/table/course_lesson_discussion_topic.yaml +35 -0
  335. package/hedhog/table/course_lesson_note.yaml +34 -0
  336. package/hedhog/table/exam.yaml +5 -0
  337. package/hedhog/table/learning_path_enrollment.yaml +6 -0
  338. package/hedhog/table/question.yaml +10 -0
  339. package/hedhog/table/question_subject.yaml +17 -0
  340. package/hedhog/table/student_activity_streak.yaml +25 -0
  341. package/package.json +7 -7
  342. package/src/achievement/achievement.controller.ts +60 -0
  343. package/src/achievement/achievement.mcp-tools.ts +108 -0
  344. package/src/achievement/achievement.module.ts +13 -0
  345. package/src/achievement/achievement.service.ts +252 -0
  346. package/src/achievement/dto/create-achievement.dto.ts +50 -0
  347. package/src/achievement/dto/update-achievement.dto.ts +47 -0
  348. package/src/bitcode-wallet/bitcode-wallet.controller.ts +69 -0
  349. package/src/bitcode-wallet/bitcode-wallet.mcp-tools.ts +107 -0
  350. package/src/bitcode-wallet/bitcode-wallet.module.ts +13 -0
  351. package/src/bitcode-wallet/bitcode-wallet.service.ts +361 -0
  352. package/src/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.ts +27 -0
  353. package/src/bitcode-wallet/dto/create-bitcode-wallet.dto.ts +7 -0
  354. package/src/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.ts +28 -0
  355. package/src/bitcode-wallet/dto/update-bitcode-wallet.dto.ts +8 -0
  356. package/src/certificate/certificate.controller.ts +20 -11
  357. package/src/certificate/certificate.mcp-tools.ts +131 -0
  358. package/src/certificate/certificate.module.ts +2 -1
  359. package/src/certificate/certificate.service.ts +95 -4
  360. package/src/certificate/dto/update-certificate-public-access.dto.ts +6 -0
  361. package/src/class-group/class-group.mcp-tools.ts +435 -0
  362. package/src/class-group/class-group.module.ts +2 -1
  363. package/src/class-group/class-group.service.ts +51 -1
  364. package/src/course/course-operations-integration.service.ts +520 -0
  365. package/src/course/course-structure.controller.ts +22 -8
  366. package/src/course/course-structure.service.ts +215 -23
  367. package/src/course/course.mcp-tools.ts +409 -0
  368. package/src/course/course.module.ts +8 -1
  369. package/src/course/course.service.ts +106 -27
  370. package/src/course/dto/create-course.dto.ts +8 -0
  371. package/src/course/dto/update-course-resources.dto.ts +39 -0
  372. package/src/course-lesson-discussion/course-lesson-discussion.controller.ts +55 -0
  373. package/src/course-lesson-discussion/course-lesson-discussion.mcp-tools.ts +75 -0
  374. package/src/course-lesson-discussion/course-lesson-discussion.module.ts +13 -0
  375. package/src/course-lesson-discussion/course-lesson-discussion.service.ts +354 -0
  376. package/src/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.ts +16 -0
  377. package/src/course-lesson-note/course-lesson-note.controller.ts +68 -0
  378. package/src/course-lesson-note/course-lesson-note.mcp-tools.ts +96 -0
  379. package/src/course-lesson-note/course-lesson-note.module.ts +13 -0
  380. package/src/course-lesson-note/course-lesson-note.service.ts +248 -0
  381. package/src/course-lesson-note/dto/create-course-lesson-note.dto.ts +16 -0
  382. package/src/course-lesson-note/dto/update-course-lesson-note.dto.ts +18 -0
  383. package/src/dashboard/dashboard.mcp-tools.ts +23 -0
  384. package/src/dashboard/dashboard.module.ts +2 -1
  385. package/src/enterprise/enterprise.mcp-tools.ts +403 -0
  386. package/src/enterprise/enterprise.module.ts +2 -1
  387. package/src/enterprise/training/enterprise-training.module.ts +11 -1
  388. package/src/enterprise/training/training-admin.mcp-tools.ts +479 -0
  389. package/src/enterprise/training/training-instructor.mcp-tools.ts +210 -0
  390. package/src/enterprise/training/training-student.controller.ts +17 -1
  391. package/src/enterprise/training/training-student.mcp-tools.ts +136 -0
  392. package/src/enterprise/training/training-student.service.ts +167 -1
  393. package/src/evaluation/evaluation.mcp-tools.ts +155 -0
  394. package/src/evaluation/evaluation.module.ts +2 -1
  395. package/src/exam/dto/create-exam-question.dto.ts +8 -0
  396. package/src/exam/dto/create-exam.dto.ts +8 -0
  397. package/src/exam/dto/create-question-subject.dto.ts +12 -0
  398. package/src/exam/exam-attempt.service.ts +46 -14
  399. package/src/exam/exam.controller.ts +19 -0
  400. package/src/exam/exam.mcp-tools.ts +337 -0
  401. package/src/exam/exam.module.ts +2 -1
  402. package/src/exam/exam.service.ts +121 -0
  403. package/src/index.ts +9 -0
  404. package/src/instructor/instructor.mcp-tools.ts +243 -0
  405. package/src/instructor/instructor.module.ts +2 -1
  406. package/src/lms.module.ts +15 -1
  407. package/src/realtime/lms-realtime.controller.ts +12 -0
  408. package/src/realtime/lms-realtime.module.ts +12 -0
  409. package/src/realtime/lms-realtime.service.ts +98 -0
  410. package/src/realtime/lms-realtime.subscriber.ts +61 -0
  411. package/src/reports/reports.mcp-tools.ts +27 -0
  412. package/src/reports/reports.module.ts +2 -1
  413. package/src/training/training.mcp-tools.ts +128 -0
  414. package/src/training/training.module.ts +2 -1
@@ -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
 
@@ -133,14 +91,6 @@ interface Turma {
133
91
  }
134
92
 
135
93
  type SessionRecurrenceFrequency = 'daily' | 'weekly' | 'monthly' | 'yearly';
136
- type SessionRecurrenceMode =
137
- | 'none'
138
- | 'daily'
139
- | 'weekly'
140
- | 'monthly'
141
- | 'yearly'
142
- | 'weekdays'
143
- | 'custom';
144
94
  type SessionRecurrenceDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
145
95
 
146
96
  type SessionRecurrenceSummary = {
@@ -203,107 +153,12 @@ type ApiCourseList = {
203
153
  lastPage?: number;
204
154
  };
205
155
 
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
156
  type Locale = {
252
157
  id?: number;
253
158
  code: string;
254
159
  name: string;
255
160
  };
256
161
 
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
162
  function getCourseIdByTitle(
308
163
  courses: Array<{ id: number; title: string }>,
309
164
  title: string
@@ -311,59 +166,6 @@ function getCourseIdByTitle(
311
166
  return courses.find((course) => course.title === title)?.id;
312
167
  }
313
168
 
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
169
  function toApiType(tipo: string) {
368
170
  if (tipo === 'presencial') return 'presential';
369
171
  if (tipo === 'hibrida') return 'hybrid';
@@ -431,130 +233,6 @@ function mapApiClass(item: ApiClass): Turma {
431
233
  };
432
234
  }
433
235
 
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
236
  type ViewMode = 'cards' | 'list';
559
237
 
560
238
  // ── Constants ─────────────────────────────────────────────────────────────────
@@ -576,103 +254,6 @@ const TIPO_ICON: Record<string, LucideIcon> = {
576
254
  };
577
255
 
578
256
  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
257
 
677
258
  // ── Animations ────────────────────────────────────────────────────────────────
678
259
 
@@ -690,26 +271,15 @@ const stagger = {
690
271
 
691
272
  export default function TurmasPage() {
692
273
  const t = useTranslations('lms.ClassesPage');
693
- const courseSheetT = useTranslations('lms.CoursesPage');
694
274
  const router = useRouter();
695
275
  const { request, currentLocaleCode, getSettingValue, locales } = useApp();
696
276
 
697
- const [sheetOpen, setSheetOpen] = useState(false);
698
- const [editingTurma, setEditingTurma] = useState<Turma | null>(null);
277
+ const [classFormSheetOpen, setClassFormSheetOpen] = useState(false);
278
+ const [classFormClassId, setClassFormClassId] = useState<string | undefined>(
279
+ undefined
280
+ );
699
281
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
700
282
  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
283
 
714
284
  // Search inputs (uncommitted)
715
285
  const [buscaInput, setBuscaInput] = useState('');
@@ -731,99 +301,21 @@ export default function TurmasPage() {
731
301
  allowedValues: PAGE_SIZES,
732
302
  });
733
303
 
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',
304
+ const { data: coursesResponse } = useQuery<ApiCourseList>({
305
+ queryKey: ['lms-courses-for-class-form'],
306
+ queryFn: async () => {
307
+ const response = await request<ApiCourseList>({
308
+ url: '/lms/courses',
309
+ method: 'GET',
310
+ params: {
311
+ page: 1,
312
+ pageSize: 500,
313
+ offeringTypes: 'scheduled,blended',
314
+ },
315
+ });
316
+ return response.data;
762
317
  },
763
318
  });
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
319
 
828
320
  const {
829
321
  data: classesResponse,
@@ -894,28 +386,6 @@ export default function TurmasPage() {
894
386
  return () => clearTimeout(timeoutId);
895
387
  }, [buscaInput]);
896
388
 
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
389
  const courseOptions = useMemo(
920
390
  () =>
921
391
  (coursesResponse?.data ?? []).map((item) => ({
@@ -930,224 +400,6 @@ export default function TurmasPage() {
930
400
  [courseOptions]
931
401
  );
932
402
 
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
403
  const professorNameById = useMemo<Map<number, string>>(() => {
1152
404
  const map = new Map<number, string>();
1153
405
  for (const turma of classesResponse?.data ?? []) {
@@ -1185,40 +437,9 @@ export default function TurmasPage() {
1185
437
  }),
1186
438
  [classesResponse, professorNameById, courseLogoMap]
1187
439
  );
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]);
440
+ const visibleTurmas = turmas;
1219
441
 
1220
442
  const totalItems = classesResponse?.total ?? 0;
1221
- const totalPages = Math.max(classesResponse?.lastPage ?? 1, 1);
1222
443
  const loading = isClassesLoading && !classesResponse;
1223
444
  const isRefreshing = isClassesFetching && !isClassesLoading;
1224
445
  const refetchersRef = useRef({
@@ -1233,12 +454,6 @@ export default function TurmasPage() {
1233
454
  };
1234
455
  }, [refetchClasses, refetchStats]);
1235
456
 
1236
- useEffect(() => {
1237
- if (currentPage > totalPages) {
1238
- setCurrentPage(totalPages);
1239
- }
1240
- }, [currentPage, totalPages]);
1241
-
1242
457
  useEffect(() => {
1243
458
  const refreshClassesData = () => {
1244
459
  void refetchersRef.current.refetchClasses();
@@ -1299,347 +514,17 @@ export default function TurmasPage() {
1299
514
  setDeleteDialogOpen(true);
1300
515
  }
1301
516
 
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
517
  // ── CRUD ──────────────────────────────────────────────────────────────────
1317
518
 
1318
519
  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);
520
+ setClassFormClassId(undefined);
521
+ setClassFormSheetOpen(true);
1352
522
  }
1353
523
 
1354
- async function openEditSheet(turma: Turma, e?: React.MouseEvent) {
524
+ function openEditSheet(turma: Turma, e?: React.MouseEvent) {
1355
525
  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
- }
526
+ setClassFormClassId(String(turma.id));
527
+ setClassFormSheetOpen(true);
1643
528
  }
1644
529
 
1645
530
  async function confirmDelete() {
@@ -1661,26 +546,6 @@ export default function TurmasPage() {
1661
546
  }
1662
547
  }
1663
548
 
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
549
  // ── KPIs ────────────────────────────────────────────────���─────────────────
1685
550
 
1686
551
  const kpis: KpiCardItem[] = [
@@ -1802,15 +667,24 @@ export default function TurmasPage() {
1802
667
  <div className="space-y-1">
1803
668
  <SearchBar
1804
669
  searchQuery={buscaInput}
1805
- onSearchChange={setBuscaInput}
1806
- onSearch={() => setBuscaDebounced(buscaInput.trim())}
670
+ onSearchChange={(value) => {
671
+ setBuscaInput(value);
672
+ setCurrentPage(1);
673
+ }}
674
+ onSearch={() => {
675
+ setBuscaDebounced(buscaInput.trim());
676
+ setCurrentPage(1);
677
+ }}
1807
678
  placeholder={t('filters.searchPlaceholder')}
1808
679
  controls={[
1809
680
  {
1810
681
  id: 'status-filter',
1811
682
  type: 'select',
1812
683
  value: filtroStatusInput,
1813
- onChange: setFiltroStatusInput,
684
+ onChange: (value) => {
685
+ setFiltroStatusInput(value);
686
+ setCurrentPage(1);
687
+ },
1814
688
  placeholder: t('filters.status'),
1815
689
  options: [
1816
690
  { value: 'todos', label: t('filters.allStatuses') },
@@ -1824,7 +698,10 @@ export default function TurmasPage() {
1824
698
  id: 'type-filter',
1825
699
  type: 'select',
1826
700
  value: filtroTipoInput,
1827
- onChange: setFiltroTipoInput,
701
+ onChange: (value) => {
702
+ setFiltroTipoInput(value);
703
+ setCurrentPage(1);
704
+ },
1828
705
  placeholder: t('filters.type'),
1829
706
  options: [
1830
707
  { value: 'todos', label: t('filters.allTypes') },
@@ -1837,7 +714,10 @@ export default function TurmasPage() {
1837
714
  id: 'course-filter',
1838
715
  type: 'select',
1839
716
  value: filtroCursoInput,
1840
- onChange: setFiltroCursoInput,
717
+ onChange: (value) => {
718
+ setFiltroCursoInput(value);
719
+ setCurrentPage(1);
720
+ },
1841
721
  placeholder: t('filters.course'),
1842
722
  options: [
1843
723
  { value: 'todos', label: t('filters.allCourses') },
@@ -1973,7 +853,7 @@ export default function TurmasPage() {
1973
853
  <motion.div key={turma.id} variants={fadeUp}>
1974
854
  <Card
1975
855
  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)}
856
+ onDoubleClick={() => openEditSheet(turma)}
1977
857
  title={t('cards.tooltip')}
1978
858
  >
1979
859
  <div
@@ -2166,7 +1046,7 @@ export default function TurmasPage() {
2166
1046
  <TableRow
2167
1047
  key={turma.id}
2168
1048
  className="cursor-pointer"
2169
- onClick={() => handleCardClick(turma)}
1049
+ onDoubleClick={() => openEditSheet(turma)}
2170
1050
  title={t('cards.tooltip')}
2171
1051
  >
2172
1052
  <TableCell>
@@ -2289,900 +1169,15 @@ export default function TurmasPage() {
2289
1169
  )}
2290
1170
  </div>
2291
1171
 
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);
1172
+ <ClassFormSheet
1173
+ open={classFormSheetOpen}
1174
+ onOpenChange={setClassFormSheetOpen}
1175
+ classId={classFormClassId}
1176
+ onSuccess={async () => {
1177
+ await Promise.all([refetchClasses(), refetchStats()]);
1178
+ notifyLmsDashboardUpdated();
3052
1179
  }}
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>
1180
+ />
3186
1181
 
3187
1182
  {/* Delete Dialog */}
3188
1183
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>