@hed-hog/lms 0.0.304 → 0.0.306

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 (458) hide show
  1. package/README.md +413 -401
  2. package/dist/certificate/certificate.controller.d.ts +90 -0
  3. package/dist/certificate/certificate.controller.d.ts.map +1 -0
  4. package/dist/certificate/certificate.controller.js +121 -0
  5. package/dist/certificate/certificate.controller.js.map +1 -0
  6. package/dist/certificate/certificate.module.d.ts +3 -0
  7. package/dist/certificate/certificate.module.d.ts.map +1 -0
  8. package/dist/certificate/certificate.module.js +26 -0
  9. package/dist/certificate/certificate.module.js.map +1 -0
  10. package/dist/certificate/certificate.service.d.ts +115 -0
  11. package/dist/certificate/certificate.service.d.ts.map +1 -0
  12. package/dist/certificate/certificate.service.js +343 -0
  13. package/dist/certificate/certificate.service.js.map +1 -0
  14. package/dist/certificate/dto/create-certificate-template.dto.d.ts +8 -0
  15. package/dist/certificate/dto/create-certificate-template.dto.d.ts.map +1 -0
  16. package/dist/certificate/dto/create-certificate-template.dto.js +44 -0
  17. package/dist/certificate/dto/create-certificate-template.dto.js.map +1 -0
  18. package/dist/certificate/dto/update-certificate-template.dto.d.ts +6 -0
  19. package/dist/certificate/dto/update-certificate-template.dto.d.ts.map +1 -0
  20. package/dist/certificate/dto/update-certificate-template.dto.js +9 -0
  21. package/dist/certificate/dto/update-certificate-template.dto.js.map +1 -0
  22. package/dist/class-group/class-group.controller.d.ts +305 -0
  23. package/dist/class-group/class-group.controller.d.ts.map +1 -0
  24. package/dist/class-group/class-group.controller.js +257 -0
  25. package/dist/class-group/class-group.controller.js.map +1 -0
  26. package/dist/class-group/class-group.module.d.ts +3 -0
  27. package/dist/class-group/class-group.module.d.ts.map +1 -0
  28. package/dist/class-group/class-group.module.js +25 -0
  29. package/dist/class-group/class-group.module.js.map +1 -0
  30. package/dist/class-group/class-group.service.d.ts +354 -0
  31. package/dist/class-group/class-group.service.d.ts.map +1 -0
  32. package/dist/class-group/class-group.service.js +1356 -0
  33. package/dist/class-group/class-group.service.js.map +1 -0
  34. package/dist/class-group/dto/create-class-group.dto.d.ts +33 -0
  35. package/dist/class-group/dto/create-class-group.dto.d.ts.map +1 -0
  36. package/dist/class-group/dto/create-class-group.dto.js +165 -0
  37. package/dist/class-group/dto/create-class-group.dto.js.map +1 -0
  38. package/dist/class-group/dto/create-session.dto.d.ts +22 -0
  39. package/dist/class-group/dto/create-session.dto.d.ts.map +1 -0
  40. package/dist/class-group/dto/create-session.dto.js +117 -0
  41. package/dist/class-group/dto/create-session.dto.js.map +1 -0
  42. package/dist/class-group/dto/enrollment.dto.d.ts +22 -0
  43. package/dist/class-group/dto/enrollment.dto.d.ts.map +1 -0
  44. package/dist/class-group/dto/enrollment.dto.js +89 -0
  45. package/dist/class-group/dto/enrollment.dto.js.map +1 -0
  46. package/dist/class-group/dto/update-class-group.dto.d.ts +6 -0
  47. package/dist/class-group/dto/update-class-group.dto.d.ts.map +1 -0
  48. package/dist/class-group/dto/update-class-group.dto.js +9 -0
  49. package/dist/class-group/dto/update-class-group.dto.js.map +1 -0
  50. package/dist/class-group/dto/update-session.dto.d.ts +7 -0
  51. package/dist/class-group/dto/update-session.dto.d.ts.map +1 -0
  52. package/dist/class-group/dto/update-session.dto.js +24 -0
  53. package/dist/class-group/dto/update-session.dto.js.map +1 -0
  54. package/dist/course/course-structure.controller.d.ts +127 -0
  55. package/dist/course/course-structure.controller.d.ts.map +1 -0
  56. package/dist/course/course-structure.controller.js +115 -0
  57. package/dist/course/course-structure.controller.js.map +1 -0
  58. package/dist/course/course-structure.service.d.ts +142 -0
  59. package/dist/course/course-structure.service.d.ts.map +1 -0
  60. package/dist/course/course-structure.service.js +445 -0
  61. package/dist/course/course-structure.service.js.map +1 -0
  62. package/dist/course/course.controller.d.ts +195 -0
  63. package/dist/course/course.controller.d.ts.map +1 -0
  64. package/dist/course/course.controller.js +104 -0
  65. package/dist/course/course.controller.js.map +1 -0
  66. package/dist/course/course.module.d.ts +3 -0
  67. package/dist/course/course.module.d.ts.map +1 -0
  68. package/dist/course/course.module.js +28 -0
  69. package/dist/course/course.module.js.map +1 -0
  70. package/dist/course/course.service.d.ts +215 -0
  71. package/dist/course/course.service.d.ts.map +1 -0
  72. package/dist/course/course.service.js +743 -0
  73. package/dist/course/course.service.js.map +1 -0
  74. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +24 -0
  75. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -0
  76. package/dist/course/dto/create-course-structure-lesson.dto.js +118 -0
  77. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -0
  78. package/dist/course/dto/create-course-structure-session.dto.d.ts +7 -0
  79. package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -0
  80. package/dist/course/dto/create-course-structure-session.dto.js +40 -0
  81. package/dist/course/dto/create-course-structure-session.dto.js.map +1 -0
  82. package/dist/course/dto/create-course.dto.d.ts +26 -0
  83. package/dist/course/dto/create-course.dto.d.ts.map +1 -0
  84. package/dist/course/dto/create-course.dto.js +138 -0
  85. package/dist/course/dto/create-course.dto.js.map +1 -0
  86. package/dist/course/dto/update-course-structure-lesson.dto.d.ts +6 -0
  87. package/dist/course/dto/update-course-structure-lesson.dto.d.ts.map +1 -0
  88. package/dist/course/dto/update-course-structure-lesson.dto.js +9 -0
  89. package/dist/course/dto/update-course-structure-lesson.dto.js.map +1 -0
  90. package/dist/course/dto/update-course-structure-session.dto.d.ts +6 -0
  91. package/dist/course/dto/update-course-structure-session.dto.d.ts.map +1 -0
  92. package/dist/course/dto/update-course-structure-session.dto.js +9 -0
  93. package/dist/course/dto/update-course-structure-session.dto.js.map +1 -0
  94. package/dist/course/dto/update-course.dto.d.ts +6 -0
  95. package/dist/course/dto/update-course.dto.d.ts.map +1 -0
  96. package/dist/course/dto/update-course.dto.js +9 -0
  97. package/dist/course/dto/update-course.dto.js.map +1 -0
  98. package/dist/dashboard/dashboard.controller.d.ts +101 -0
  99. package/dist/dashboard/dashboard.controller.d.ts.map +1 -0
  100. package/dist/dashboard/dashboard.controller.js +40 -0
  101. package/dist/dashboard/dashboard.controller.js.map +1 -0
  102. package/dist/dashboard/dashboard.module.d.ts +3 -0
  103. package/dist/dashboard/dashboard.module.d.ts.map +1 -0
  104. package/dist/dashboard/dashboard.module.js +25 -0
  105. package/dist/dashboard/dashboard.module.js.map +1 -0
  106. package/dist/dashboard/dashboard.service.d.ts +130 -0
  107. package/dist/dashboard/dashboard.service.d.ts.map +1 -0
  108. package/dist/dashboard/dashboard.service.js +626 -0
  109. package/dist/dashboard/dashboard.service.js.map +1 -0
  110. package/dist/enterprise/dto/add-enterprise-class-group.dto.d.ts +4 -0
  111. package/dist/enterprise/dto/add-enterprise-class-group.dto.d.ts.map +1 -0
  112. package/dist/enterprise/dto/add-enterprise-class-group.dto.js +22 -0
  113. package/dist/enterprise/dto/add-enterprise-class-group.dto.js.map +1 -0
  114. package/dist/enterprise/dto/add-enterprise-course.dto.d.ts +5 -0
  115. package/dist/enterprise/dto/add-enterprise-course.dto.d.ts.map +1 -0
  116. package/dist/enterprise/dto/add-enterprise-course.dto.js +27 -0
  117. package/dist/enterprise/dto/add-enterprise-course.dto.js.map +1 -0
  118. package/dist/enterprise/dto/add-enterprise-student.dto.d.ts +5 -0
  119. package/dist/enterprise/dto/add-enterprise-student.dto.d.ts.map +1 -0
  120. package/dist/enterprise/dto/add-enterprise-student.dto.js +27 -0
  121. package/dist/enterprise/dto/add-enterprise-student.dto.js.map +1 -0
  122. package/dist/enterprise/dto/add-enterprise-user.dto.d.ts +7 -0
  123. package/dist/enterprise/dto/add-enterprise-user.dto.d.ts.map +1 -0
  124. package/dist/enterprise/dto/add-enterprise-user.dto.js +36 -0
  125. package/dist/enterprise/dto/add-enterprise-user.dto.js.map +1 -0
  126. package/dist/enterprise/dto/create-enterprise.dto.d.ts +10 -0
  127. package/dist/enterprise/dto/create-enterprise.dto.d.ts.map +1 -0
  128. package/dist/enterprise/dto/create-enterprise.dto.js +54 -0
  129. package/dist/enterprise/dto/create-enterprise.dto.js.map +1 -0
  130. package/dist/enterprise/dto/update-enterprise-student.dto.d.ts +4 -0
  131. package/dist/enterprise/dto/update-enterprise-student.dto.d.ts.map +1 -0
  132. package/dist/enterprise/dto/update-enterprise-student.dto.js +22 -0
  133. package/dist/enterprise/dto/update-enterprise-student.dto.js.map +1 -0
  134. package/dist/enterprise/dto/update-enterprise-user.dto.d.ts +5 -0
  135. package/dist/enterprise/dto/update-enterprise-user.dto.d.ts.map +1 -0
  136. package/dist/enterprise/dto/update-enterprise-user.dto.js +27 -0
  137. package/dist/enterprise/dto/update-enterprise-user.dto.js.map +1 -0
  138. package/dist/enterprise/dto/update-enterprise.dto.d.ts +6 -0
  139. package/dist/enterprise/dto/update-enterprise.dto.d.ts.map +1 -0
  140. package/dist/enterprise/dto/update-enterprise.dto.js +9 -0
  141. package/dist/enterprise/dto/update-enterprise.dto.js.map +1 -0
  142. package/dist/enterprise/enterprise.controller.d.ts +269 -0
  143. package/dist/enterprise/enterprise.controller.d.ts.map +1 -0
  144. package/dist/enterprise/enterprise.controller.js +311 -0
  145. package/dist/enterprise/enterprise.controller.js.map +1 -0
  146. package/dist/enterprise/enterprise.module.d.ts +3 -0
  147. package/dist/enterprise/enterprise.module.d.ts.map +1 -0
  148. package/dist/enterprise/enterprise.module.js +25 -0
  149. package/dist/enterprise/enterprise.module.js.map +1 -0
  150. package/dist/enterprise/enterprise.service.d.ts +282 -0
  151. package/dist/enterprise/enterprise.service.d.ts.map +1 -0
  152. package/dist/enterprise/enterprise.service.js +627 -0
  153. package/dist/enterprise/enterprise.service.js.map +1 -0
  154. package/dist/evaluation/evaluation.controller.d.ts +56 -0
  155. package/dist/evaluation/evaluation.controller.d.ts.map +1 -0
  156. package/dist/evaluation/evaluation.controller.js +76 -0
  157. package/dist/evaluation/evaluation.controller.js.map +1 -0
  158. package/dist/evaluation/evaluation.module.d.ts +3 -0
  159. package/dist/evaluation/evaluation.module.d.ts.map +1 -0
  160. package/dist/evaluation/evaluation.module.js +25 -0
  161. package/dist/evaluation/evaluation.module.js.map +1 -0
  162. package/dist/evaluation/evaluation.service.d.ts +67 -0
  163. package/dist/evaluation/evaluation.service.d.ts.map +1 -0
  164. package/dist/evaluation/evaluation.service.js +378 -0
  165. package/dist/evaluation/evaluation.service.js.map +1 -0
  166. package/dist/exam/dto/create-exam-question.dto.d.ts +25 -0
  167. package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -0
  168. package/dist/exam/dto/create-exam-question.dto.js +117 -0
  169. package/dist/exam/dto/create-exam-question.dto.js.map +1 -0
  170. package/dist/exam/dto/create-exam.dto.d.ts +11 -0
  171. package/dist/exam/dto/create-exam.dto.d.ts.map +1 -0
  172. package/dist/exam/dto/create-exam.dto.js +63 -0
  173. package/dist/exam/dto/create-exam.dto.js.map +1 -0
  174. package/dist/exam/dto/reorder-exam-questions.dto.d.ts +4 -0
  175. package/dist/exam/dto/reorder-exam-questions.dto.d.ts.map +1 -0
  176. package/dist/exam/dto/reorder-exam-questions.dto.js +23 -0
  177. package/dist/exam/dto/reorder-exam-questions.dto.js.map +1 -0
  178. package/dist/exam/dto/save-exam-attempt-answers.dto.d.ts +14 -0
  179. package/dist/exam/dto/save-exam-attempt-answers.dto.d.ts.map +1 -0
  180. package/dist/exam/dto/save-exam-attempt-answers.dto.js +68 -0
  181. package/dist/exam/dto/save-exam-attempt-answers.dto.js.map +1 -0
  182. package/dist/exam/dto/start-exam-attempt.dto.d.ts +4 -0
  183. package/dist/exam/dto/start-exam-attempt.dto.d.ts.map +1 -0
  184. package/dist/exam/dto/start-exam-attempt.dto.js +23 -0
  185. package/dist/exam/dto/start-exam-attempt.dto.js.map +1 -0
  186. package/dist/exam/dto/submit-exam-attempt.dto.d.ts +5 -0
  187. package/dist/exam/dto/submit-exam-attempt.dto.d.ts.map +1 -0
  188. package/dist/exam/dto/submit-exam-attempt.dto.js +23 -0
  189. package/dist/exam/dto/submit-exam-attempt.dto.js.map +1 -0
  190. package/dist/exam/dto/update-exam-question.dto.d.ts +6 -0
  191. package/dist/exam/dto/update-exam-question.dto.d.ts.map +1 -0
  192. package/dist/exam/dto/update-exam-question.dto.js +9 -0
  193. package/dist/exam/dto/update-exam-question.dto.js.map +1 -0
  194. package/dist/exam/dto/update-exam.dto.d.ts +6 -0
  195. package/dist/exam/dto/update-exam.dto.d.ts.map +1 -0
  196. package/dist/exam/dto/update-exam.dto.js +9 -0
  197. package/dist/exam/dto/update-exam.dto.js.map +1 -0
  198. package/dist/exam/exam-attempt.controller.d.ts +273 -0
  199. package/dist/exam/exam-attempt.controller.d.ts.map +1 -0
  200. package/dist/exam/exam-attempt.controller.js +84 -0
  201. package/dist/exam/exam-attempt.controller.js.map +1 -0
  202. package/dist/exam/exam-attempt.service.d.ts +302 -0
  203. package/dist/exam/exam-attempt.service.d.ts.map +1 -0
  204. package/dist/exam/exam-attempt.service.js +776 -0
  205. package/dist/exam/exam-attempt.service.js.map +1 -0
  206. package/dist/exam/exam.controller.d.ts +162 -0
  207. package/dist/exam/exam.controller.d.ts.map +1 -0
  208. package/dist/exam/exam.controller.js +158 -0
  209. package/dist/exam/exam.controller.js.map +1 -0
  210. package/dist/exam/exam.module.d.ts +3 -0
  211. package/dist/exam/exam.module.d.ts.map +1 -0
  212. package/dist/exam/exam.module.js +27 -0
  213. package/dist/exam/exam.module.js.map +1 -0
  214. package/dist/exam/exam.service.d.ts +179 -0
  215. package/dist/exam/exam.service.d.ts.map +1 -0
  216. package/dist/exam/exam.service.js +597 -0
  217. package/dist/exam/exam.service.js.map +1 -0
  218. package/dist/index.d.ts +28 -0
  219. package/dist/index.d.ts.map +1 -1
  220. package/dist/index.js +28 -0
  221. package/dist/index.js.map +1 -1
  222. package/dist/instructor/dto/create-instructor.dto.d.ts +10 -0
  223. package/dist/instructor/dto/create-instructor.dto.d.ts.map +1 -0
  224. package/dist/instructor/dto/create-instructor.dto.js +55 -0
  225. package/dist/instructor/dto/create-instructor.dto.js.map +1 -0
  226. package/dist/instructor/dto/update-instructor.dto.d.ts +9 -0
  227. package/dist/instructor/dto/update-instructor.dto.d.ts.map +1 -0
  228. package/dist/instructor/dto/update-instructor.dto.js +51 -0
  229. package/dist/instructor/dto/update-instructor.dto.js.map +1 -0
  230. package/dist/instructor/instructor.controller.d.ts +52 -0
  231. package/dist/instructor/instructor.controller.d.ts.map +1 -0
  232. package/dist/instructor/instructor.controller.js +98 -0
  233. package/dist/instructor/instructor.controller.js.map +1 -0
  234. package/dist/instructor/instructor.module.d.ts +3 -0
  235. package/dist/instructor/instructor.module.d.ts.map +1 -0
  236. package/dist/instructor/instructor.module.js +25 -0
  237. package/dist/instructor/instructor.module.js.map +1 -0
  238. package/dist/instructor/instructor.service.d.ts +79 -0
  239. package/dist/instructor/instructor.service.d.ts.map +1 -0
  240. package/dist/instructor/instructor.service.js +528 -0
  241. package/dist/instructor/instructor.service.js.map +1 -0
  242. package/dist/lms.module.d.ts.map +1 -1
  243. package/dist/lms.module.js +36 -4
  244. package/dist/lms.module.js.map +1 -1
  245. package/dist/reports/reports.controller.d.ts +69 -0
  246. package/dist/reports/reports.controller.d.ts.map +1 -0
  247. package/dist/reports/reports.controller.js +40 -0
  248. package/dist/reports/reports.controller.js.map +1 -0
  249. package/dist/reports/reports.module.d.ts +3 -0
  250. package/dist/reports/reports.module.d.ts.map +1 -0
  251. package/dist/reports/reports.module.js +25 -0
  252. package/dist/reports/reports.module.js.map +1 -0
  253. package/dist/reports/reports.service.d.ts +80 -0
  254. package/dist/reports/reports.service.d.ts.map +1 -0
  255. package/dist/reports/reports.service.js +366 -0
  256. package/dist/reports/reports.service.js.map +1 -0
  257. package/dist/training/dto/create-training.dto.d.ts +19 -0
  258. package/dist/training/dto/create-training.dto.d.ts.map +1 -0
  259. package/dist/training/dto/create-training.dto.js +98 -0
  260. package/dist/training/dto/create-training.dto.js.map +1 -0
  261. package/dist/training/dto/update-training.dto.d.ts +6 -0
  262. package/dist/training/dto/update-training.dto.d.ts.map +1 -0
  263. package/dist/training/dto/update-training.dto.js +9 -0
  264. package/dist/training/dto/update-training.dto.js.map +1 -0
  265. package/dist/training/training.controller.d.ts +195 -0
  266. package/dist/training/training.controller.d.ts.map +1 -0
  267. package/dist/training/training.controller.js +104 -0
  268. package/dist/training/training.controller.js.map +1 -0
  269. package/dist/training/training.module.d.ts +3 -0
  270. package/dist/training/training.module.d.ts.map +1 -0
  271. package/dist/training/training.module.js +25 -0
  272. package/dist/training/training.module.js.map +1 -0
  273. package/dist/training/training.service.d.ts +213 -0
  274. package/dist/training/training.service.d.ts.map +1 -0
  275. package/dist/training/training.service.js +497 -0
  276. package/dist/training/training.service.js.map +1 -0
  277. package/hedhog/data/dashboard.yaml +6 -0
  278. package/hedhog/data/dashboard_component.yaml +153 -0
  279. package/hedhog/data/dashboard_component_role.yaml +97 -0
  280. package/hedhog/data/dashboard_item.yaml +167 -0
  281. package/hedhog/data/dashboard_role.yaml +6 -0
  282. package/hedhog/data/instructor_qualification.yaml +16 -0
  283. package/hedhog/data/menu.yaml +129 -19
  284. package/hedhog/data/role.yaml +25 -1
  285. package/hedhog/data/route.yaml +867 -0
  286. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +1992 -0
  287. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +480 -0
  288. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  289. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +164 -0
  290. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +120 -0
  291. package/hedhog/frontend/app/_components/lms-class-calendar.tsx.ejs +272 -0
  292. package/hedhog/frontend/app/_components/mobile-calendar.tsx.ejs +277 -0
  293. package/hedhog/frontend/app/_lib/editor/canvasInstance.ts.ejs +48 -0
  294. package/hedhog/frontend/app/_lib/editor/pctHelpers.ts.ejs +50 -0
  295. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +268 -0
  296. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +94 -0
  297. package/hedhog/frontend/app/_lib/store/useTemplateStore.ts.ejs +284 -0
  298. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +638 -0
  299. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +916 -0
  300. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +200 -0
  301. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +769 -0
  302. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +104 -0
  303. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +354 -0
  304. package/hedhog/frontend/app/certificates/models/editor/page.tsx.ejs +5 -0
  305. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +883 -0
  306. package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +279 -0
  307. package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +1027 -0
  308. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +3130 -993
  309. package/hedhog/frontend/app/classes/page.tsx.ejs +2731 -759
  310. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +80 -0
  311. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +226 -0
  312. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +71 -0
  313. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +42 -0
  314. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +111 -0
  315. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +113 -0
  316. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +215 -0
  317. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +236 -0
  318. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +141 -0
  319. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +57 -0
  320. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  321. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +33 -0
  322. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +933 -1103
  323. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +699 -117
  324. package/hedhog/frontend/app/courses/page.tsx.ejs +1018 -1042
  325. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +317 -0
  326. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-panel.tsx.ejs +88 -0
  327. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +318 -0
  328. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +332 -0
  329. package/hedhog/frontend/app/enterprise/_components/enterprise-class-create-sheet.tsx.ejs +58 -0
  330. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +390 -0
  331. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +112 -0
  332. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +183 -0
  333. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +363 -0
  334. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-constants.ts.ejs +88 -0
  335. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +548 -0
  336. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-utils.ts.ejs +33 -0
  337. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  338. package/hedhog/frontend/app/enterprise/_components/enterprise-person-picker.ts.ejs +31 -0
  339. package/hedhog/frontend/app/enterprise/_components/enterprise-progress-bar.tsx.ejs +21 -0
  340. package/hedhog/frontend/app/enterprise/_components/enterprise-related-tab.tsx.ejs +224 -0
  341. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +397 -0
  342. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +167 -0
  343. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +267 -0
  344. package/hedhog/frontend/app/enterprise/_components/enterprise-system-user-picker.ts.ejs +42 -0
  345. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +96 -0
  346. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +207 -0
  347. package/hedhog/frontend/app/enterprise/_components/enterprise-user-distribution-chart.tsx.ejs +149 -0
  348. package/hedhog/frontend/app/enterprise/page.tsx.ejs +596 -0
  349. package/hedhog/frontend/app/evaluations/page.tsx.ejs +1250 -0
  350. package/hedhog/frontend/app/exams/[id]/attempt/page.tsx.ejs +642 -196
  351. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +11 -0
  352. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +1316 -436
  353. package/hedhog/frontend/app/exams/page.tsx.ejs +799 -546
  354. package/hedhog/frontend/app/layout.tsx.ejs +5 -0
  355. package/hedhog/frontend/app/page.tsx.ejs +3 -1220
  356. package/hedhog/frontend/app/reports/courses/page.tsx.ejs +843 -0
  357. package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +890 -0
  358. package/hedhog/frontend/app/reports/page.tsx.ejs +802 -808
  359. package/hedhog/frontend/app/reports/students/page.tsx.ejs +772 -0
  360. package/hedhog/frontend/app/training/page.tsx.ejs +1873 -628
  361. package/hedhog/frontend/messages/en.json +1606 -111
  362. package/hedhog/frontend/messages/pt.json +1636 -134
  363. package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +74 -0
  364. package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +74 -0
  365. package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +81 -0
  366. package/hedhog/frontend/widgets/category-distribution-chart.tsx.ejs +119 -0
  367. package/hedhog/frontend/widgets/class-calendar.tsx.ejs +440 -0
  368. package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +81 -0
  369. package/hedhog/frontend/widgets/engagement-chart.tsx.ejs +120 -0
  370. package/hedhog/frontend/widgets/footer-summary.tsx.ejs +80 -0
  371. package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +74 -0
  372. package/hedhog/frontend/widgets/latest-enrollments.tsx.ejs +166 -0
  373. package/hedhog/frontend/widgets/student-growth-chart.tsx.ejs +89 -0
  374. package/hedhog/frontend/widgets/top-courses-chart.tsx.ejs +104 -0
  375. package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +78 -0
  376. package/hedhog/frontend/widgets/upcoming-classes.tsx.ejs +152 -0
  377. package/hedhog/table/course.yaml +19 -1
  378. package/hedhog/table/course_class_group.yaml +8 -0
  379. package/hedhog/table/course_class_session.yaml +33 -0
  380. package/hedhog/table/course_instructor.yaml +27 -0
  381. package/hedhog/table/enterprise.yaml +29 -0
  382. package/hedhog/table/enterprise_class_group.yaml +20 -0
  383. package/hedhog/table/enterprise_course.yaml +23 -0
  384. package/hedhog/table/enterprise_student.yaml +24 -0
  385. package/hedhog/table/enterprise_user.yaml +35 -0
  386. package/hedhog/table/instructor_qualification.yaml +26 -0
  387. package/hedhog/table/instructor_qualification_assignment.yaml +22 -0
  388. package/hedhog/table/question.yaml +6 -0
  389. package/package.json +6 -6
  390. package/src/certificate/certificate.controller.ts +83 -0
  391. package/src/certificate/certificate.module.ts +13 -0
  392. package/src/certificate/certificate.service.ts +413 -0
  393. package/src/certificate/dto/create-certificate-template.dto.ts +25 -0
  394. package/src/certificate/dto/update-certificate-template.dto.ts +6 -0
  395. package/src/class-group/class-group.controller.ts +189 -0
  396. package/src/class-group/class-group.module.ts +12 -0
  397. package/src/class-group/class-group.service.ts +1802 -0
  398. package/src/class-group/dto/create-class-group.dto.ts +139 -0
  399. package/src/class-group/dto/create-session.dto.ts +102 -0
  400. package/src/class-group/dto/enrollment.dto.ts +70 -0
  401. package/src/class-group/dto/update-class-group.dto.ts +4 -0
  402. package/src/class-group/dto/update-session.dto.ts +9 -0
  403. package/src/course/course-structure.controller.ts +85 -0
  404. package/src/course/course-structure.service.ts +525 -0
  405. package/src/course/course.controller.ts +69 -0
  406. package/src/course/course.module.ts +15 -0
  407. package/src/course/course.service.ts +920 -0
  408. package/src/course/dto/create-course-structure-lesson.dto.ts +97 -0
  409. package/src/course/dto/create-course-structure-session.dto.ts +22 -0
  410. package/src/course/dto/create-course.dto.ts +111 -0
  411. package/src/course/dto/update-course-structure-lesson.dto.ts +6 -0
  412. package/src/course/dto/update-course-structure-session.dto.ts +6 -0
  413. package/src/course/dto/update-course.dto.ts +4 -0
  414. package/src/dashboard/dashboard.controller.ts +14 -0
  415. package/src/dashboard/dashboard.module.ts +12 -0
  416. package/src/dashboard/dashboard.service.ts +726 -0
  417. package/src/enterprise/dto/add-enterprise-class-group.dto.ts +7 -0
  418. package/src/enterprise/dto/add-enterprise-course.dto.ts +11 -0
  419. package/src/enterprise/dto/add-enterprise-student.dto.ts +16 -0
  420. package/src/enterprise/dto/add-enterprise-user.dto.ts +23 -0
  421. package/src/enterprise/dto/create-enterprise.dto.ts +41 -0
  422. package/src/enterprise/dto/update-enterprise-student.dto.ts +7 -0
  423. package/src/enterprise/dto/update-enterprise-user.dto.ts +11 -0
  424. package/src/enterprise/dto/update-enterprise.dto.ts +4 -0
  425. package/src/enterprise/enterprise.controller.ts +233 -0
  426. package/src/enterprise/enterprise.module.ts +12 -0
  427. package/src/enterprise/enterprise.service.ts +712 -0
  428. package/src/evaluation/evaluation.controller.ts +44 -0
  429. package/src/evaluation/evaluation.module.ts +12 -0
  430. package/src/evaluation/evaluation.service.ts +394 -0
  431. package/src/exam/dto/create-exam-question.dto.ts +103 -0
  432. package/src/exam/dto/create-exam.dto.ts +41 -0
  433. package/src/exam/dto/reorder-exam-questions.dto.ts +8 -0
  434. package/src/exam/dto/save-exam-attempt-answers.dto.ts +55 -0
  435. package/src/exam/dto/start-exam-attempt.dto.ts +8 -0
  436. package/src/exam/dto/submit-exam-attempt.dto.ts +8 -0
  437. package/src/exam/dto/update-exam-question.dto.ts +4 -0
  438. package/src/exam/dto/update-exam.dto.ts +4 -0
  439. package/src/exam/exam-attempt.controller.ts +65 -0
  440. package/src/exam/exam-attempt.service.ts +1008 -0
  441. package/src/exam/exam.controller.ts +102 -0
  442. package/src/exam/exam.module.ts +14 -0
  443. package/src/exam/exam.service.ts +784 -0
  444. package/src/index.ts +29 -0
  445. package/src/instructor/dto/create-instructor.dto.ts +43 -0
  446. package/src/instructor/dto/update-instructor.dto.ts +38 -0
  447. package/src/instructor/instructor.controller.ts +73 -0
  448. package/src/instructor/instructor.module.ts +12 -0
  449. package/src/instructor/instructor.service.ts +646 -0
  450. package/src/lms.module.ts +36 -4
  451. package/src/reports/reports.controller.ts +14 -0
  452. package/src/reports/reports.module.ts +12 -0
  453. package/src/reports/reports.service.ts +485 -0
  454. package/src/training/dto/create-training.dto.ts +81 -0
  455. package/src/training/dto/update-training.dto.ts +4 -0
  456. package/src/training/training.controller.ts +68 -0
  457. package/src/training/training.module.ts +12 -0
  458. package/src/training/training.service.ts +574 -0
@@ -5,7 +5,10 @@ import {
5
5
  Page,
6
6
  PageHeader,
7
7
  PaginationFooter,
8
+ SearchBar,
9
+ ViewModeToggle,
8
10
  } from '@/components/entity-list';
11
+ import { RichTextEditor } from '@/components/rich-text-editor';
9
12
  import { Badge } from '@/components/ui/badge';
10
13
  import { Button } from '@/components/ui/button';
11
14
  import { Card, CardContent } from '@/components/ui/card';
@@ -17,8 +20,30 @@ import {
17
20
  DialogHeader,
18
21
  DialogTitle,
19
22
  } from '@/components/ui/dialog';
20
- import { Field, FieldLabel } from '@/components/ui/field';
23
+ import {
24
+ DropdownMenu,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuSeparator,
28
+ DropdownMenuTrigger,
29
+ } from '@/components/ui/dropdown-menu';
30
+ import {
31
+ Form,
32
+ FormControl,
33
+ FormField,
34
+ FormItem,
35
+ FormLabel,
36
+ FormMessage,
37
+ } from '@/components/ui/form';
21
38
  import { Input } from '@/components/ui/input';
39
+ import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
40
+ import {
41
+ Select,
42
+ SelectContent,
43
+ SelectItem,
44
+ SelectTrigger,
45
+ SelectValue,
46
+ } from '@/components/ui/select';
22
47
  import { Separator } from '@/components/ui/separator';
23
48
  import {
24
49
  Sheet,
@@ -29,7 +54,15 @@ import {
29
54
  SheetTitle,
30
55
  } from '@/components/ui/sheet';
31
56
  import { Skeleton } from '@/components/ui/skeleton';
32
- import { Textarea } from '@/components/ui/textarea';
57
+ import {
58
+ Table,
59
+ TableBody,
60
+ TableCell,
61
+ TableHead,
62
+ TableHeader,
63
+ TableRow,
64
+ } from '@/components/ui/table';
65
+ import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
33
66
  import {
34
67
  closestCenter,
35
68
  DndContext,
@@ -47,6 +80,7 @@ import {
47
80
  verticalListSortingStrategy,
48
81
  } from '@dnd-kit/sortable';
49
82
  import { CSS } from '@dnd-kit/utilities';
83
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
50
84
  import { zodResolver } from '@hookform/resolvers/zod';
51
85
  import { AnimatePresence, motion } from 'framer-motion';
52
86
  import {
@@ -59,6 +93,8 @@ import {
59
93
  Copy,
60
94
  FileCheck,
61
95
  GripVertical,
96
+ Loader2,
97
+ MoreHorizontal,
62
98
  Pencil,
63
99
  Plus,
64
100
  Save,
@@ -66,27 +102,94 @@ import {
66
102
  X,
67
103
  } from 'lucide-react';
68
104
  import { useTranslations } from 'next-intl';
69
- import { useParams, usePathname } from 'next/navigation';
70
- import { useEffect, useState } from 'react';
105
+ import { useParams, useRouter } from 'next/navigation';
106
+ import { useEffect, useMemo, useState } from 'react';
71
107
  import { useForm } from 'react-hook-form';
72
108
  import { toast } from 'sonner';
73
109
  import { z } from 'zod';
74
110
 
75
111
  interface Alternativa {
76
- id: string;
112
+ id: string | number;
77
113
  texto: string;
78
114
  correta: boolean;
79
115
  }
80
116
 
81
- interface Questao {
117
+ interface FillBlankAnswer {
118
+ id: string;
119
+ answer: string;
120
+ alternativesText: string;
121
+ }
122
+
123
+ interface MatchingPair {
82
124
  id: string;
125
+ leftText: string;
126
+ rightText: string;
127
+ }
128
+
129
+ interface Questao {
130
+ id: number;
131
+ examQuestionId: number;
132
+ order: number;
133
+ tipo: ExamQuestionType;
83
134
  enunciado: string;
84
135
  pontuacao: number;
85
136
  alternativas: Alternativa[];
137
+ fillBlankAnswers: FillBlankAnswer[];
138
+ matchingPairs: MatchingPair[];
86
139
  }
87
140
 
141
+ type ExamQuestionType =
142
+ | 'multiple_choice'
143
+ | 'true_false'
144
+ | 'essay'
145
+ | 'fill_blank'
146
+ | 'matching';
147
+
148
+ type ViewMode = 'cards' | 'list';
149
+
150
+ const OBJECTIVE_QUESTION_TYPES: ExamQuestionType[] = [
151
+ 'multiple_choice',
152
+ 'true_false',
153
+ ];
154
+
155
+ type ApiExamQuestionAlternative = {
156
+ id: number;
157
+ text: string;
158
+ isCorrect: boolean;
159
+ };
160
+
161
+ type ApiExamQuestion = {
162
+ id: number;
163
+ examQuestionId: number;
164
+ order: number;
165
+ questionType: ExamQuestionType;
166
+ statement: string;
167
+ points: number;
168
+ alternatives: ApiExamQuestionAlternative[];
169
+ fillBlankAnswers?: Array<{
170
+ answer: string;
171
+ alternatives?: string[];
172
+ }>;
173
+ matchingPairs?: Array<{
174
+ id: string;
175
+ leftText: string;
176
+ rightText: string;
177
+ }>;
178
+ };
179
+
180
+ type ApiExamQuestionsResponse = {
181
+ exam: {
182
+ id: number;
183
+ code: string;
184
+ title: string;
185
+ };
186
+ questionCount: number;
187
+ data: ApiExamQuestion[];
188
+ };
189
+
88
190
  type QuestaoForm = {
89
191
  enunciado: string;
192
+ questionType: ExamQuestionType;
90
193
  pontuacao: number;
91
194
  };
92
195
 
@@ -94,86 +197,38 @@ function generateId() {
94
197
  return Math.random().toString(36).substring(2, 9);
95
198
  }
96
199
 
97
- const initialQuestoes: Questao[] = [
98
- {
99
- id: 'q1',
100
- enunciado:
101
- 'Qual hook do React e utilizado para gerenciar estado local em componentes funcionais?',
102
- pontuacao: 2,
103
- alternativas: [
104
- { id: 'a1', texto: 'useEffect', correta: false },
105
- { id: 'a2', texto: 'useState', correta: true },
106
- { id: 'a3', texto: 'useContext', correta: false },
107
- { id: 'a4', texto: 'useReducer', correta: false },
108
- ],
109
- },
110
- {
111
- id: 'q2',
112
- enunciado: 'O que o Virtual DOM faz no React?',
113
- pontuacao: 2,
114
- alternativas: [
115
- { id: 'a5', texto: 'Substitui o DOM real completamente', correta: false },
116
- {
117
- id: 'a6',
118
- texto:
119
- 'Cria uma copia do DOM para comparar e atualizar apenas o necessario',
120
- correta: true,
121
- },
122
- { id: 'a7', texto: 'Elimina a necessidade de HTML', correta: false },
123
- ],
124
- },
125
- {
126
- id: 'q3',
127
- enunciado: 'Qual e a principal diferenca entre props e state?',
128
- pontuacao: 3,
129
- alternativas: [
130
- {
131
- id: 'a8',
132
- texto: 'Props sao mutaveis, state sao imutaveis',
133
- correta: false,
134
- },
135
- {
136
- id: 'a9',
137
- texto: 'Props sao passados pelo pai, state e gerenciado internamente',
138
- correta: true,
139
- },
140
- { id: 'a10', texto: 'Nao ha diferenca', correta: false },
141
- {
142
- id: 'a11',
143
- texto: 'State so existe em class components',
144
- correta: false,
145
- },
146
- { id: 'a12', texto: 'Props nao podem ser funcoes', correta: false },
147
- ],
148
- },
149
- {
150
- id: 'q4',
151
- enunciado:
152
- 'Qual metodo do ciclo de vida e equivalente ao useEffect com array de dependencias vazio?',
153
- pontuacao: 2,
154
- alternativas: [
155
- { id: 'a13', texto: 'componentDidMount', correta: true },
156
- { id: 'a14', texto: 'componentWillUpdate', correta: false },
157
- { id: 'a15', texto: 'componentDidUpdate', correta: false },
158
- ],
159
- },
160
- {
161
- id: 'q5',
162
- enunciado: 'Para que serve o React.memo?',
163
- pontuacao: 1,
164
- alternativas: [
165
- { id: 'a16', texto: 'Para memorizar valores computados', correta: false },
166
- {
167
- id: 'a17',
168
- texto: 'Para evitar re-renderizacoes desnecessarias de componentes',
169
- correta: true,
170
- },
171
- ],
172
- },
173
- ];
200
+ function mapApiQuestion(question: ApiExamQuestion): Questao {
201
+ return {
202
+ id: question.id,
203
+ examQuestionId: question.examQuestionId,
204
+ order: question.order,
205
+ tipo: question.questionType,
206
+ enunciado: question.statement,
207
+ pontuacao: question.points,
208
+ alternativas: question.alternatives.map((alternative) => ({
209
+ id: alternative.id,
210
+ texto: alternative.text,
211
+ correta: alternative.isCorrect,
212
+ })),
213
+ fillBlankAnswers: (question.fillBlankAnswers ?? []).map((item, index) => ({
214
+ id: `blank-${index + 1}`,
215
+ answer: item.answer,
216
+ alternativesText: (item.alternatives ?? []).join(', '),
217
+ })),
218
+ matchingPairs: (question.matchingPairs ?? []).map((pair, index) => ({
219
+ id: pair.id || `pair-${index + 1}`,
220
+ leftText: pair.leftText,
221
+ rightText: pair.rightText,
222
+ })),
223
+ };
224
+ }
174
225
 
175
226
  const PAGE_SIZE_OPTIONS = [6, 12, 24];
176
227
 
228
+ function stripHtml(html: string): string {
229
+ return html.replace(/<[^>]*>/g, '').trim();
230
+ }
231
+
177
232
  function SortableAlternativa({
178
233
  alt,
179
234
  index,
@@ -181,6 +236,7 @@ function SortableAlternativa({
181
236
  onChangeTexto,
182
237
  onRemove,
183
238
  canRemove,
239
+ disableTextInput,
184
240
  }: {
185
241
  alt: Alternativa;
186
242
  index: number;
@@ -188,6 +244,7 @@ function SortableAlternativa({
188
244
  onChangeTexto: (texto: string) => void;
189
245
  onRemove: () => void;
190
246
  canRemove: boolean;
247
+ disableTextInput?: boolean;
191
248
  }) {
192
249
  const t = useTranslations('lms.QuestionsPage');
193
250
  const {
@@ -231,6 +288,7 @@ function SortableAlternativa({
231
288
  onChange={(e) => onChangeTexto(e.target.value)}
232
289
  className="flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0"
233
290
  placeholder={t('sheet.fields.alternativePlaceholder')}
291
+ disabled={disableTextInput}
234
292
  />
235
293
 
236
294
  <button
@@ -307,129 +365,202 @@ function SortableQuestao({
307
365
  animate={{ opacity: 1, y: 0 }}
308
366
  exit={{ opacity: 0, y: -10 }}
309
367
  transition={{ duration: 0.2 }}
310
- className={isDragging ? 'z-50' : ''}
368
+ className={`w-full ${isDragging ? 'z-50' : ''}`}
311
369
  >
312
- <Card className="overflow-hidden">
370
+ <Card className="group relative overflow-hidden border-border/70 shadow-sm transition-all duration-200 hover:border-border hover:shadow-md">
371
+ <div className="absolute inset-x-0 top-0 h-1 bg-primary/20" />
313
372
  {/* Question header */}
314
- <div className="flex items-start gap-3 p-4 transition-colors hover:bg-muted/30">
315
- <button
316
- type="button"
317
- className="cursor-grab touch-none text-muted-foreground hover:text-foreground"
318
- {...attributes}
319
- {...listeners}
320
- aria-label={t('question.dragQuestion')}
321
- >
322
- <GripVertical className="size-5" />
323
- </button>
324
-
325
- <div
326
- className="flex min-w-0 flex-1 cursor-pointer items-start gap-3"
327
- onClick={onToggleExpand}
328
- role="button"
329
- aria-expanded={isExpanded}
330
- tabIndex={0}
331
- onKeyDown={(e) => {
332
- if (e.key === 'Enter' || e.key === ' ') {
333
- e.preventDefault();
334
- onToggleExpand();
335
- }
336
- }}
337
- >
338
- <div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-foreground text-sm font-bold text-background">
339
- {qIndex + 1}
340
- </div>
341
- <div className="min-w-0 flex-1">
342
- <p className="text-sm font-medium leading-relaxed">
343
- {questao.enunciado}
344
- </p>
345
- <div className="mt-2 flex flex-wrap items-center gap-2">
346
- <Badge variant="secondary" className="text-xs">
347
- {t('question.alternatives', {
348
- count: questao.alternativas.length,
349
- })}
350
- </Badge>
351
- <Badge variant="outline" className="text-xs">
352
- {questao.pontuacao === 1
353
- ? t('question.points', { score: questao.pontuacao })
354
- : t('question.pointsPlural', { score: questao.pontuacao })}
355
- </Badge>
356
- </div>
357
- </div>
358
- <div className="shrink-0">
359
- {isExpanded ? (
360
- <ChevronUp className="size-4 text-muted-foreground" />
361
- ) : (
362
- <ChevronDown className="size-4 text-muted-foreground" />
363
- )}
364
- </div>
365
- </div>
366
- </div>
367
-
368
- {/* Expanded content */}
369
- <AnimatePresence>
370
- {isExpanded && (
371
- <motion.div
372
- initial={{ height: 0, opacity: 0 }}
373
- animate={{ height: 'auto', opacity: 1 }}
374
- exit={{ height: 0, opacity: 0 }}
375
- transition={{ duration: 0.25 }}
376
- className="overflow-hidden"
373
+ <CardContent className="p-4">
374
+ <div className="flex items-start gap-3 transition-colors">
375
+ <button
376
+ type="button"
377
+ className="mt-0.5 cursor-grab touch-none text-muted-foreground"
378
+ {...attributes}
379
+ {...listeners}
380
+ aria-label={t('question.dragQuestion')}
377
381
  >
378
- <Separator />
379
- <div className="p-4">
380
- <div className="flex flex-col gap-2">
381
- {questao.alternativas.map((alt, altIndex) => (
382
- <div
383
- key={alt.id}
384
- className={`flex items-center gap-3 rounded-lg border p-3 text-sm ${
385
- alt.correta
386
- ? 'border-foreground/20 bg-muted/50 font-medium'
387
- : ''
388
- }`}
389
- >
390
- <span className="flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium">
391
- {String.fromCharCode(65 + altIndex)}
392
- </span>
393
- <span className="flex-1">{alt.texto}</span>
394
- {alt.correta && (
395
- <Check className="size-4 shrink-0 text-foreground" />
396
- )}
397
- </div>
398
- ))}
382
+ <GripVertical className="size-5" />
383
+ </button>
384
+
385
+ <div
386
+ className="min-w-0 flex-1 cursor-pointer"
387
+ onClick={onToggleExpand}
388
+ role="button"
389
+ aria-expanded={isExpanded}
390
+ tabIndex={0}
391
+ onKeyDown={(e) => {
392
+ if (e.key === 'Enter' || e.key === ' ') {
393
+ e.preventDefault();
394
+ onToggleExpand();
395
+ }
396
+ }}
397
+ >
398
+ <div className="mb-1 flex items-start justify-between gap-2">
399
+ <div className="min-w-0 flex-1">
400
+ <p className="line-clamp-2 text-sm font-semibold leading-snug text-foreground">
401
+ {stripHtml(questao.enunciado)}
402
+ </p>
403
+ <p className="mt-1 text-xs text-muted-foreground">
404
+ #{qIndex + 1}
405
+ </p>
399
406
  </div>
407
+ <DropdownMenu>
408
+ <DropdownMenuTrigger asChild>
409
+ <Button
410
+ variant="ghost"
411
+ size="icon"
412
+ className="size-8 shrink-0 -mr-2 -mt-1"
413
+ onClick={(e) => e.stopPropagation()}
414
+ aria-label={t('table.actions.label')}
415
+ >
416
+ <MoreHorizontal className="size-4" />
417
+ </Button>
418
+ </DropdownMenuTrigger>
419
+ <DropdownMenuContent align="end" className="w-48">
420
+ <DropdownMenuItem onClick={onEdit}>
421
+ <Pencil className="size-3.5" />{' '}
422
+ {t('question.actions.edit')}
423
+ </DropdownMenuItem>
424
+ <DropdownMenuItem onClick={onDuplicate}>
425
+ <Copy className="mr-2 size-4" />{' '}
426
+ {t('question.actions.duplicate')}
427
+ </DropdownMenuItem>
428
+ <DropdownMenuSeparator />
429
+ <DropdownMenuItem
430
+ className="text-destructive focus:text-destructive"
431
+ onClick={onDelete}
432
+ >
433
+ <Trash2 className="size-3.5" />{' '}
434
+ {t('question.actions.delete')}
435
+ </DropdownMenuItem>
436
+ </DropdownMenuContent>
437
+ </DropdownMenu>
438
+ </div>
400
439
 
401
- <div className="mt-4 flex flex-wrap gap-2">
402
- <Button
403
- variant="outline"
404
- size="sm"
405
- className="gap-1.5"
406
- onClick={onEdit}
407
- >
408
- <Pencil className="size-3.5" /> {t('question.actions.edit')}
409
- </Button>
410
- <Button
411
- variant="outline"
412
- size="sm"
413
- className="gap-1.5"
414
- onClick={onDuplicate}
415
- >
416
- <Copy className="size-3.5" />{' '}
417
- {t('question.actions.duplicate')}
418
- </Button>
419
- <Button
420
- variant="outline"
421
- size="sm"
422
- className="gap-1.5 text-destructive hover:text-destructive"
423
- onClick={onDelete}
424
- >
425
- <Trash2 className="size-3.5" />{' '}
426
- {t('question.actions.delete')}
427
- </Button>
440
+ <div className="flex items-start justify-between gap-3">
441
+ <div className="mt-2 flex flex-wrap items-center gap-2">
442
+ <Badge variant="outline" className="text-xs">
443
+ {t(`question.types.${questao.tipo}`)}
444
+ </Badge>
445
+ <Badge variant="secondary" className="text-xs">
446
+ {questao.tipo === 'fill_blank'
447
+ ? t('question.fillBlankAnswers', {
448
+ count: questao.fillBlankAnswers.length,
449
+ })
450
+ : questao.tipo === 'matching'
451
+ ? t('question.matchingPairs', {
452
+ count: questao.matchingPairs.length,
453
+ })
454
+ : t('question.alternatives', {
455
+ count: questao.alternativas.length,
456
+ })}
457
+ </Badge>
458
+ <Badge variant="outline" className="text-xs">
459
+ {questao.pontuacao === 1
460
+ ? t('question.points', { score: questao.pontuacao })
461
+ : t('question.pointsPlural', {
462
+ score: questao.pontuacao,
463
+ })}
464
+ </Badge>
465
+ </div>
466
+ <div className="shrink-0 pt-2">
467
+ {isExpanded ? (
468
+ <ChevronUp className="size-4 text-muted-foreground" />
469
+ ) : (
470
+ <ChevronDown className="size-4 text-muted-foreground" />
471
+ )}
428
472
  </div>
429
473
  </div>
430
- </motion.div>
431
- )}
432
- </AnimatePresence>
474
+ </div>
475
+ </div>
476
+
477
+ {/* Expanded content */}
478
+ <AnimatePresence>
479
+ {isExpanded && (
480
+ <motion.div
481
+ initial={{ height: 0, opacity: 0 }}
482
+ animate={{ height: 'auto', opacity: 1 }}
483
+ exit={{ height: 0, opacity: 0 }}
484
+ transition={{ duration: 0.25 }}
485
+ className="overflow-hidden"
486
+ >
487
+ <Separator />
488
+ <div className="p-4">
489
+ <div className="flex flex-col gap-2">
490
+ {questao.tipo === 'fill_blank' ? (
491
+ questao.fillBlankAnswers.length > 0 ? (
492
+ questao.fillBlankAnswers.map((item) => (
493
+ <div
494
+ key={item.id}
495
+ className="rounded-lg border p-3 text-sm"
496
+ >
497
+ <p>
498
+ <span className="font-medium">
499
+ {t('sheet.fields.fillBlankExpectedAnswer')}:
500
+ </span>{' '}
501
+ {item.answer}
502
+ </p>
503
+ {item.alternativesText.length > 0 && (
504
+ <p className="mt-1 text-xs text-muted-foreground">
505
+ {t('sheet.fields.fillBlankAlternativeAnswers')}:{' '}
506
+ {item.alternativesText}
507
+ </p>
508
+ )}
509
+ </div>
510
+ ))
511
+ ) : (
512
+ <div className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground">
513
+ {t('question.noFillBlankAnswers')}
514
+ </div>
515
+ )
516
+ ) : questao.tipo === 'matching' ? (
517
+ questao.matchingPairs.length > 0 ? (
518
+ questao.matchingPairs.map((pair) => (
519
+ <div
520
+ key={pair.id}
521
+ className="rounded-lg border p-3 text-sm"
522
+ >
523
+ <p className="font-medium">{pair.leftText}</p>
524
+ <p className="mt-1 text-muted-foreground">
525
+ {pair.rightText}
526
+ </p>
527
+ </div>
528
+ ))
529
+ ) : (
530
+ <div className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground">
531
+ {t('question.noMatchingPairs')}
532
+ </div>
533
+ )
534
+ ) : questao.alternativas.length > 0 ? (
535
+ questao.alternativas.map((alt, altIndex) => (
536
+ <div
537
+ key={alt.id}
538
+ className={`flex items-center gap-3 rounded-lg border p-3 text-sm ${
539
+ alt.correta
540
+ ? 'border-foreground/20 bg-muted/50 font-medium'
541
+ : ''
542
+ }`}
543
+ >
544
+ <span className="flex size-6 shrink-0 items-center justify-center rounded-full border text-xs font-medium">
545
+ {String.fromCharCode(65 + altIndex)}
546
+ </span>
547
+ <span className="flex-1">{alt.texto}</span>
548
+ {alt.correta && (
549
+ <Check className="size-4 shrink-0 text-foreground" />
550
+ )}
551
+ </div>
552
+ ))
553
+ ) : (
554
+ <div className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground">
555
+ {t('question.noAlternatives')}
556
+ </div>
557
+ )}
558
+ </div>
559
+ </div>
560
+ </motion.div>
561
+ )}
562
+ </AnimatePresence>
563
+ </CardContent>
433
564
  </Card>
434
565
  </motion.div>
435
566
  );
@@ -437,30 +568,80 @@ function SortableQuestao({
437
568
 
438
569
  export default function QuestoesPage() {
439
570
  const t = useTranslations('lms.QuestionsPage');
440
- const { id } = useParams();
441
- const pathname = usePathname();
442
- const [loading, setLoading] = useState(true);
443
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
444
- const [questoes, setQuestoes] = useState<Questao[]>(initialQuestoes);
445
- const [currentPage, setCurrentPage] = useState(1);
446
- const [pageSize, setPageSize] = useState(6);
447
- const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
571
+ const { request } = useApp();
572
+ const router = useRouter();
573
+ const { id } = useParams<{ id: string }>();
574
+ const examId = Number(id);
575
+ const isValidExamId = Number.isFinite(examId) && examId > 0;
576
+
577
+ const [questoes, setQuestoes] = useState<Questao[]>([]);
578
+ const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
448
579
  const [sheetOpen, setSheetOpen] = useState(false);
449
580
  const [editingQuestao, setEditingQuestao] = useState<Questao | null>(null);
450
581
  const [sheetAlternativas, setSheetAlternativas] = useState<Alternativa[]>([]);
582
+ const [sheetFillBlankAnswers, setSheetFillBlankAnswers] = useState<
583
+ FillBlankAnswer[]
584
+ >([]);
585
+ const [sheetMatchingPairs, setSheetMatchingPairs] = useState<MatchingPair[]>(
586
+ []
587
+ );
451
588
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
452
589
  const [questaoToDelete, setQuestaoToDelete] = useState<Questao | null>(null);
453
590
  const [hasChanges, setHasChanges] = useState(false);
591
+ const [buscaInput, setBuscaInput] = useState('');
592
+ const [debouncedBuscaInput, setDebouncedBuscaInput] = useState('');
593
+ const [filtroTipoInput, setFiltroTipoInput] = useState<
594
+ ExamQuestionType | 'todos'
595
+ >('todos');
596
+ const [viewMode, setViewMode] = usePersistedViewMode<ViewMode>({
597
+ storageKey: 'lms:exam-questions:view-mode',
598
+ defaultValue: 'cards',
599
+ allowedValues: ['cards', 'list'],
600
+ });
601
+ const [currentPage, setCurrentPage] = useState(1);
602
+ const [pageSize, setPageSize] = useState(12);
454
603
 
455
604
  const questaoSchema = z.object({
456
605
  enunciado: z.string().min(5, t('sheet.validation.statementMin')),
606
+ questionType: z.enum([
607
+ 'multiple_choice',
608
+ 'true_false',
609
+ 'essay',
610
+ 'fill_blank',
611
+ 'matching',
612
+ ]),
457
613
  pontuacao: z.coerce.number().min(0.5, t('sheet.validation.scoreMin')),
458
614
  });
459
615
 
460
616
  const form = useForm<QuestaoForm>({
461
617
  resolver: zodResolver(questaoSchema),
462
- defaultValues: { enunciado: '', pontuacao: 2 },
618
+ defaultValues: {
619
+ enunciado: '',
620
+ questionType: 'multiple_choice',
621
+ pontuacao: 1,
622
+ },
463
623
  });
624
+ const selectedQuestionType = form.watch('questionType');
625
+
626
+ const {
627
+ data: questionData,
628
+ isLoading,
629
+ isFetching,
630
+ refetch: refetchQuestions,
631
+ } = useQuery<ApiExamQuestionsResponse>({
632
+ queryKey: ['lms-exam-questions', examId],
633
+ enabled: isValidExamId,
634
+ queryFn: async () => {
635
+ const response = await request<ApiExamQuestionsResponse>({
636
+ url: `/lms/exams/${examId}/questions`,
637
+ method: 'GET',
638
+ });
639
+
640
+ return response.data;
641
+ },
642
+ });
643
+
644
+ const loading = isLoading || isFetching;
464
645
 
465
646
  const sensors = useSensors(
466
647
  useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
@@ -468,18 +649,126 @@ export default function QuestoesPage() {
468
649
  );
469
650
 
470
651
  useEffect(() => {
471
- const timer = setTimeout(() => setLoading(false), 600);
472
- return () => clearTimeout(timer);
473
- }, []);
474
-
475
- const totalPages = Math.max(1, Math.ceil(questoes.length / pageSize));
476
- const safePage = Math.min(Math.max(currentPage, 1), totalPages);
477
- const paginatedQuestoes = questoes.slice(
478
- (safePage - 1) * pageSize,
479
- safePage * pageSize
480
- );
652
+ if (!questionData) return;
653
+ setQuestoes(
654
+ (questionData.data ?? []).map((question) => mapApiQuestion(question))
655
+ );
656
+ setHasChanges(false);
657
+ }, [questionData]);
658
+
659
+ useEffect(() => {
660
+ const timeoutId = setTimeout(() => {
661
+ setDebouncedBuscaInput(buscaInput.trim());
662
+ }, 300);
663
+
664
+ return () => clearTimeout(timeoutId);
665
+ }, [buscaInput]);
666
+
667
+ const filteredQuestoes = useMemo(() => {
668
+ const normalizedSearch = debouncedBuscaInput.toLowerCase();
669
+
670
+ return questoes.filter((questao) => {
671
+ const matchesType =
672
+ filtroTipoInput === 'todos' || questao.tipo === filtroTipoInput;
673
+ const matchesSearch =
674
+ normalizedSearch.length === 0 ||
675
+ questao.enunciado.toLowerCase().includes(normalizedSearch);
676
+
677
+ return matchesType && matchesSearch;
678
+ });
679
+ }, [debouncedBuscaInput, filtroTipoInput, questoes]);
680
+
681
+ const totalPages = Math.max(1, Math.ceil(filteredQuestoes.length / pageSize));
682
+ const safePage = Math.min(currentPage, totalPages);
683
+ const paginatedQuestoes = useMemo(() => {
684
+ const startIndex = (safePage - 1) * pageSize;
685
+ return filteredQuestoes.slice(startIndex, startIndex + pageSize);
686
+ }, [filteredQuestoes, pageSize, safePage]);
687
+
688
+ const hasActiveFilters =
689
+ debouncedBuscaInput.length > 0 || filtroTipoInput !== 'todos';
690
+
691
+ function clearFilters() {
692
+ setBuscaInput('');
693
+ setDebouncedBuscaInput('');
694
+ setFiltroTipoInput('todos');
695
+ setCurrentPage(1);
696
+ }
697
+
698
+ useEffect(() => {
699
+ if (currentPage > totalPages) {
700
+ setCurrentPage(totalPages);
701
+ }
702
+ }, [currentPage, totalPages]);
703
+
704
+ useEffect(() => {
705
+ if (selectedQuestionType === 'fill_blank') {
706
+ setSheetAlternativas([]);
707
+ setSheetMatchingPairs([]);
708
+ setSheetFillBlankAnswers((prev) => {
709
+ if (prev.length > 0) {
710
+ return prev;
711
+ }
481
712
 
482
- function toggleExpand(qId: string) {
713
+ return [{ id: generateId(), answer: '', alternativesText: '' }];
714
+ });
715
+ return;
716
+ }
717
+
718
+ if (selectedQuestionType === 'matching') {
719
+ setSheetAlternativas([]);
720
+ setSheetFillBlankAnswers([]);
721
+ setSheetMatchingPairs((prev) => {
722
+ if (prev.length >= 2) {
723
+ return prev;
724
+ }
725
+
726
+ return [
727
+ { id: generateId(), leftText: '', rightText: '' },
728
+ { id: generateId(), leftText: '', rightText: '' },
729
+ ];
730
+ });
731
+ return;
732
+ }
733
+
734
+ if (selectedQuestionType === 'essay') {
735
+ setSheetAlternativas([]);
736
+ setSheetFillBlankAnswers([]);
737
+ setSheetMatchingPairs([]);
738
+ return;
739
+ }
740
+
741
+ if (selectedQuestionType === 'true_false') {
742
+ setSheetFillBlankAnswers([]);
743
+ setSheetMatchingPairs([]);
744
+ setSheetAlternativas((prev) => {
745
+ if (editingQuestao?.tipo === 'true_false' && prev.length === 2) {
746
+ return prev;
747
+ }
748
+
749
+ return [
750
+ { id: 'true', texto: t('sheet.fields.trueLabel'), correta: true },
751
+ { id: 'false', texto: t('sheet.fields.falseLabel'), correta: false },
752
+ ];
753
+ });
754
+ return;
755
+ }
756
+
757
+ setSheetAlternativas((prev) => {
758
+ if (prev.length >= 2) {
759
+ return prev;
760
+ }
761
+
762
+ return [
763
+ { id: generateId(), texto: '', correta: true },
764
+ { id: generateId(), texto: '', correta: false },
765
+ ];
766
+ });
767
+ setSheetFillBlankAnswers([]);
768
+ setSheetMatchingPairs([]);
769
+ }, [editingQuestao?.tipo, selectedQuestionType, t]);
770
+
771
+ function toggleExpand(qId: number) {
483
772
  setExpandedIds((prev) => {
484
773
  const next = new Set(prev);
485
774
  if (next.has(qId)) next.delete(qId);
@@ -490,44 +779,104 @@ export default function QuestoesPage() {
490
779
 
491
780
  function openCreateSheet() {
492
781
  setEditingQuestao(null);
493
- form.reset({ enunciado: '', pontuacao: 2 });
782
+ form.reset({
783
+ enunciado: '',
784
+ questionType: 'multiple_choice',
785
+ pontuacao: 1,
786
+ });
494
787
  setSheetAlternativas([
495
788
  { id: generateId(), texto: '', correta: true },
496
789
  { id: generateId(), texto: '', correta: false },
497
790
  ]);
791
+ setSheetFillBlankAnswers([]);
792
+ setSheetMatchingPairs([]);
498
793
  setSheetOpen(true);
499
794
  }
500
795
 
501
796
  function openEditSheet(questao: Questao) {
502
797
  setEditingQuestao(questao);
503
- form.reset({ enunciado: questao.enunciado, pontuacao: questao.pontuacao });
798
+ form.reset({
799
+ enunciado: questao.enunciado,
800
+ questionType: questao.tipo,
801
+ pontuacao: questao.pontuacao,
802
+ });
504
803
  setSheetAlternativas(questao.alternativas.map((a) => ({ ...a })));
804
+ setSheetFillBlankAnswers(
805
+ questao.fillBlankAnswers.map((item) => ({ ...item }))
806
+ );
807
+ setSheetMatchingPairs(questao.matchingPairs.map((item) => ({ ...item })));
505
808
  setSheetOpen(true);
506
809
  }
507
810
 
508
811
  function addAlternativa() {
812
+ if (selectedQuestionType !== 'multiple_choice') {
813
+ return;
814
+ }
815
+
509
816
  setSheetAlternativas((prev) => [
510
817
  ...prev,
511
818
  { id: generateId(), texto: '', correta: false },
512
819
  ]);
513
820
  }
514
821
 
515
- function removeAlternativa(altId: string) {
822
+ function removeAlternativa(altId: string | number) {
516
823
  setSheetAlternativas((prev) => prev.filter((a) => a.id !== altId));
517
824
  }
518
825
 
519
- function toggleCorreta(altId: string) {
826
+ function toggleCorreta(altId: string | number) {
520
827
  setSheetAlternativas((prev) =>
521
828
  prev.map((a) => ({ ...a, correta: a.id === altId }))
522
829
  );
523
830
  }
524
831
 
525
- function updateAlternativaTexto(altId: string, texto: string) {
832
+ function updateAlternativaTexto(altId: string | number, texto: string) {
526
833
  setSheetAlternativas((prev) =>
527
834
  prev.map((a) => (a.id === altId ? { ...a, texto } : a))
528
835
  );
529
836
  }
530
837
 
838
+ function addFillBlankAnswer() {
839
+ setSheetFillBlankAnswers((prev) => [
840
+ ...prev,
841
+ { id: generateId(), answer: '', alternativesText: '' },
842
+ ]);
843
+ }
844
+
845
+ function removeFillBlankAnswer(id: string) {
846
+ setSheetFillBlankAnswers((prev) => prev.filter((item) => item.id !== id));
847
+ }
848
+
849
+ function updateFillBlankAnswer(
850
+ id: string,
851
+ field: 'answer' | 'alternativesText',
852
+ value: string
853
+ ) {
854
+ setSheetFillBlankAnswers((prev) =>
855
+ prev.map((item) => (item.id === id ? { ...item, [field]: value } : item))
856
+ );
857
+ }
858
+
859
+ function addMatchingPair() {
860
+ setSheetMatchingPairs((prev) => [
861
+ ...prev,
862
+ { id: generateId(), leftText: '', rightText: '' },
863
+ ]);
864
+ }
865
+
866
+ function removeMatchingPair(id: string) {
867
+ setSheetMatchingPairs((prev) => prev.filter((item) => item.id !== id));
868
+ }
869
+
870
+ function updateMatchingPair(
871
+ id: string,
872
+ field: 'leftText' | 'rightText',
873
+ value: string
874
+ ) {
875
+ setSheetMatchingPairs((prev) =>
876
+ prev.map((item) => (item.id === id ? { ...item, [field]: value } : item))
877
+ );
878
+ }
879
+
531
880
  function handleAltDragEnd(event: DragEndEvent) {
532
881
  const { active, over } = event;
533
882
  if (over && active.id !== over.id) {
@@ -539,81 +888,174 @@ export default function QuestoesPage() {
539
888
  }
540
889
  }
541
890
 
542
- function onSubmit(data: QuestaoForm) {
543
- const hasCorreta = sheetAlternativas.some((a) => a.correta);
544
- const allHaveText = sheetAlternativas.every(
545
- (a) => a.texto.trim().length > 0
891
+ function handleMatchingPairDragEnd(event: DragEndEvent) {
892
+ const { active, over } = event;
893
+ if (over && active.id !== over.id) {
894
+ setSheetMatchingPairs((items) => {
895
+ const oldIndex = items.findIndex((i) => i.id === active.id);
896
+ const newIndex = items.findIndex((i) => i.id === over.id);
897
+ return arrayMove(items, oldIndex, newIndex);
898
+ });
899
+ }
900
+ }
901
+
902
+ async function onSubmit(data: QuestaoForm) {
903
+ const requiresAlternatives = OBJECTIVE_QUESTION_TYPES.includes(
904
+ data.questionType
546
905
  );
547
906
 
548
- if (!hasCorreta) {
549
- toast.error(t('toasts.noCorrectAnswer'));
550
- return;
907
+ if (requiresAlternatives) {
908
+ const hasCorreta = sheetAlternativas.some((a) => a.correta);
909
+ const allHaveText = sheetAlternativas.every(
910
+ (a) => a.texto.trim().length > 0
911
+ );
912
+
913
+ if (!hasCorreta) {
914
+ toast.error(t('toasts.noCorrectAnswer'));
915
+ return;
916
+ }
917
+ if (!allHaveText) {
918
+ toast.error(t('toasts.allMustHaveText'));
919
+ return;
920
+ }
921
+ if (sheetAlternativas.length < 2) {
922
+ toast.error(t('toasts.minAlternatives'));
923
+ return;
924
+ }
551
925
  }
552
- if (!allHaveText) {
553
- toast.error(t('toasts.allMustHaveText'));
554
- return;
926
+
927
+ if (data.questionType === 'fill_blank') {
928
+ const normalizedFillBlank = sheetFillBlankAnswers.map((item) => ({
929
+ answer: item.answer.trim(),
930
+ alternatives: item.alternativesText
931
+ .split(',')
932
+ .map((value) => value.trim())
933
+ .filter((value) => value.length > 0),
934
+ }));
935
+
936
+ if (
937
+ normalizedFillBlank.length === 0 ||
938
+ normalizedFillBlank.some((item) => item.answer.length === 0)
939
+ ) {
940
+ toast.error(t('toasts.fillBlankNeedsAnswers'));
941
+ return;
942
+ }
555
943
  }
556
- if (sheetAlternativas.length < 2) {
557
- toast.error(t('toasts.minAlternatives'));
558
- return;
944
+
945
+ if (data.questionType === 'matching') {
946
+ const normalizedPairs = sheetMatchingPairs.map((pair) => ({
947
+ id: pair.id.trim(),
948
+ leftText: pair.leftText.trim(),
949
+ rightText: pair.rightText.trim(),
950
+ }));
951
+
952
+ if (
953
+ normalizedPairs.length < 2 ||
954
+ normalizedPairs.some(
955
+ (pair) => pair.leftText.length === 0 || pair.rightText.length === 0
956
+ )
957
+ ) {
958
+ toast.error(t('toasts.matchingNeedsPairs'));
959
+ return;
960
+ }
559
961
  }
560
962
 
963
+ const payload = {
964
+ statement: data.enunciado,
965
+ questionType: data.questionType,
966
+ points: Math.max(1, Math.round(data.pontuacao)),
967
+ ...(requiresAlternatives && {
968
+ alternatives: sheetAlternativas.map((alternative) => ({
969
+ text: alternative.texto.trim(),
970
+ isCorrect: alternative.correta,
971
+ })),
972
+ }),
973
+ ...(data.questionType === 'fill_blank' && {
974
+ fillBlankAnswers: sheetFillBlankAnswers.map((item) => ({
975
+ answer: item.answer.trim(),
976
+ alternatives: item.alternativesText
977
+ .split(',')
978
+ .map((value) => value.trim())
979
+ .filter((value) => value.length > 0),
980
+ })),
981
+ }),
982
+ ...(data.questionType === 'matching' && {
983
+ matchingPairs: sheetMatchingPairs.map((pair) => ({
984
+ id: pair.id,
985
+ leftText: pair.leftText.trim(),
986
+ rightText: pair.rightText.trim(),
987
+ })),
988
+ }),
989
+ };
990
+
561
991
  if (editingQuestao) {
562
- setQuestoes((prev) =>
563
- prev.map((q) =>
564
- q.id === editingQuestao.id
565
- ? {
566
- ...q,
567
- enunciado: data.enunciado,
568
- pontuacao: data.pontuacao,
569
- alternativas: sheetAlternativas,
570
- }
571
- : q
572
- )
573
- );
992
+ await request({
993
+ url: `/lms/exams/${examId}/questions/${editingQuestao.id}`,
994
+ method: 'PATCH',
995
+ data: payload,
996
+ });
574
997
  toast.success(t('toasts.questionUpdated'));
575
998
  } else {
576
- const nova: Questao = {
577
- id: generateId(),
578
- enunciado: data.enunciado,
579
- pontuacao: data.pontuacao,
580
- alternativas: sheetAlternativas,
581
- };
582
- setQuestoes((prev) => [...prev, nova]);
999
+ await request({
1000
+ url: `/lms/exams/${examId}/questions`,
1001
+ method: 'POST',
1002
+ data: payload,
1003
+ });
583
1004
  toast.success(t('toasts.questionCreated'));
584
1005
  }
585
- setHasChanges(true);
1006
+
1007
+ await refetchQuestions();
586
1008
  setSheetOpen(false);
587
1009
  }
588
1010
 
589
- function confirmDelete() {
1011
+ async function confirmDelete() {
590
1012
  if (questaoToDelete) {
591
- setQuestoes((prev) => prev.filter((q) => q.id !== questaoToDelete.id));
592
- setHasChanges(true);
1013
+ await request({
1014
+ url: `/lms/exams/${examId}/questions/${questaoToDelete.id}`,
1015
+ method: 'DELETE',
1016
+ });
593
1017
  toast.success(t('toasts.questionRemoved'));
594
1018
  setQuestaoToDelete(null);
595
1019
  setDeleteDialogOpen(false);
1020
+ await refetchQuestions();
596
1021
  }
597
1022
  }
598
1023
 
599
- function duplicateQuestao(questao: Questao) {
600
- const nova: Questao = {
601
- ...questao,
602
- id: generateId(),
603
- enunciado: `${questao.enunciado}${t('copySuffix')}`,
604
- alternativas: questao.alternativas.map((a) => ({
605
- ...a,
606
- id: generateId(),
607
- })),
608
- };
609
- setQuestoes((prev) => {
610
- const idx = prev.findIndex((q) => q.id === questao.id);
611
- const next = [...prev];
612
- next.splice(idx + 1, 0, nova);
613
- return next;
1024
+ async function duplicateQuestao(questao: Questao) {
1025
+ await request({
1026
+ url: `/lms/exams/${examId}/questions`,
1027
+ method: 'POST',
1028
+ data: {
1029
+ statement: `${questao.enunciado}${t('copySuffix')}`,
1030
+ questionType: questao.tipo,
1031
+ points: Math.max(1, Math.round(questao.pontuacao)),
1032
+ ...(OBJECTIVE_QUESTION_TYPES.includes(questao.tipo) && {
1033
+ alternatives: questao.alternativas.map((alternative) => ({
1034
+ text: alternative.texto,
1035
+ isCorrect: alternative.correta,
1036
+ })),
1037
+ }),
1038
+ ...(questao.tipo === 'fill_blank' && {
1039
+ fillBlankAnswers: questao.fillBlankAnswers.map((item) => ({
1040
+ answer: item.answer,
1041
+ alternatives: item.alternativesText
1042
+ .split(',')
1043
+ .map((value) => value.trim())
1044
+ .filter((value) => value.length > 0),
1045
+ })),
1046
+ }),
1047
+ ...(questao.tipo === 'matching' && {
1048
+ matchingPairs: questao.matchingPairs.map((pair) => ({
1049
+ id: pair.id,
1050
+ leftText: pair.leftText,
1051
+ rightText: pair.rightText,
1052
+ })),
1053
+ }),
1054
+ },
614
1055
  });
615
- setHasChanges(true);
1056
+
616
1057
  toast.success(t('toasts.questionDuplicated'));
1058
+ await refetchQuestions();
617
1059
  }
618
1060
 
619
1061
  function handleQuestionDragEnd(event: DragEndEvent) {
@@ -628,19 +1070,113 @@ export default function QuestoesPage() {
628
1070
  }
629
1071
  }
630
1072
 
631
- function handleSave() {
1073
+ async function handleSave() {
1074
+ await request({
1075
+ url: `/lms/exams/${examId}/questions/order`,
1076
+ method: 'PATCH',
1077
+ data: {
1078
+ questionIds: questoes.map((question) => question.id),
1079
+ },
1080
+ });
1081
+
632
1082
  setHasChanges(false);
633
1083
  toast.success(t('toasts.changesSaved'));
1084
+ await refetchQuestions();
634
1085
  }
635
1086
 
636
1087
  const totalPontuacao = questoes.reduce((a, q) => a + q.pontuacao, 0);
1088
+ const examCode = questionData?.exam?.code ?? `EX-${examId}`;
1089
+ const objectiveQuestionsCount = questoes.filter((question) =>
1090
+ OBJECTIVE_QUESTION_TYPES.includes(question.tipo)
1091
+ ).length;
1092
+ const stats: KpiCardItem[] = [
1093
+ {
1094
+ key: 'questions',
1095
+ title: t('stats.questions'),
1096
+ value: questoes.length,
1097
+ description: questionData?.exam?.title ?? examCode,
1098
+ icon: FileCheck,
1099
+ iconContainerClassName: 'bg-sky-500/10 text-sky-700',
1100
+ accentClassName: 'from-sky-500/25 via-blue-500/10 to-transparent',
1101
+ layout: 'compact',
1102
+ },
1103
+ {
1104
+ key: 'total-score',
1105
+ title: t('stats.totalScore'),
1106
+ value: totalPontuacao,
1107
+ description: t('sheet.fields.score'),
1108
+ icon: Save,
1109
+ iconContainerClassName: 'bg-emerald-500/10 text-emerald-700',
1110
+ accentClassName: 'from-emerald-500/25 via-emerald-500/10 to-transparent',
1111
+ layout: 'compact',
1112
+ },
1113
+ {
1114
+ key: 'avg-alternatives',
1115
+ title: t('stats.avgAlternatives'),
1116
+ value:
1117
+ questoes.length > 0
1118
+ ? (
1119
+ questoes.reduce((a, q) => a + q.alternativas.length, 0) /
1120
+ questoes.length
1121
+ ).toFixed(1)
1122
+ : '0',
1123
+ description: t('sheet.fields.alternatives'),
1124
+ icon: CircleDot,
1125
+ iconContainerClassName: 'bg-amber-500/10 text-amber-700',
1126
+ accentClassName: 'from-amber-500/25 via-amber-500/10 to-transparent',
1127
+ layout: 'compact',
1128
+ },
1129
+ {
1130
+ key: 'objective-questions',
1131
+ title: t('sheet.fields.questionType'),
1132
+ value: objectiveQuestionsCount,
1133
+ description: t('question.types.multiple_choice'),
1134
+ icon: Check,
1135
+ iconContainerClassName: 'bg-violet-500/10 text-violet-700',
1136
+ accentClassName: 'from-violet-500/25 via-violet-500/10 to-transparent',
1137
+ layout: 'compact',
1138
+ },
1139
+ ];
1140
+
1141
+ if (!isValidExamId) {
1142
+ return (
1143
+ <Page>
1144
+ <PageHeader
1145
+ title={t('title')}
1146
+ description="O identificador da prova e invalido ou esta ausente."
1147
+ breadcrumbs={[
1148
+ {
1149
+ label: t('breadcrumbs.home'),
1150
+ href: '/',
1151
+ },
1152
+ {
1153
+ label: t('breadcrumbs.exams'),
1154
+ href: '/lms/exams',
1155
+ },
1156
+ {
1157
+ label: t('breadcrumbs.questions'),
1158
+ },
1159
+ ]}
1160
+ />
1161
+
1162
+ <EmptyState
1163
+ icon={<AlertTriangle className="h-12 w-12" />}
1164
+ title="Nao foi possivel localizar esta prova"
1165
+ description="Confira o link acessado ou volte para a lista de provas."
1166
+ actionLabel={t('breadcrumbs.exams')}
1167
+ onAction={() => router.push('/lms/exams')}
1168
+ className="py-20"
1169
+ />
1170
+ </Page>
1171
+ );
1172
+ }
637
1173
 
638
1174
  return (
639
1175
  <Page>
640
1176
  <PageHeader
641
1177
  title={t('title')}
642
1178
  description={t('description', {
643
- code: 'EX-001',
1179
+ code: examCode,
644
1180
  count: questoes.length,
645
1181
  total: totalPontuacao,
646
1182
  })}
@@ -657,82 +1193,121 @@ export default function QuestoesPage() {
657
1193
  label: t('breadcrumbs.questions'),
658
1194
  },
659
1195
  ]}
660
- actions={
661
- <div className="flex items-center gap-2">
662
- <AnimatePresence>
663
- {hasChanges && (
664
- <motion.div
665
- initial={{ opacity: 0, scale: 0.9 }}
666
- animate={{ opacity: 1, scale: 1 }}
667
- exit={{ opacity: 0, scale: 0.9 }}
668
- >
669
- <Button
670
- onClick={handleSave}
671
- className="gap-2"
672
- variant="outline"
673
- >
674
- <Save className="size-4" /> {t('actions.save')}
675
- </Button>
676
- </motion.div>
677
- )}
678
- </AnimatePresence>
679
- <Button onClick={openCreateSheet} className="gap-2">
680
- <Plus className="size-4" /> {t('actions.newQuestion')}
681
- </Button>
682
- </div>
683
- }
1196
+ actions={[
1197
+ {
1198
+ label: t('actions.save'),
1199
+ onClick: handleSave,
1200
+ icon: <Save className="size-4" />,
1201
+ variant: 'outline',
1202
+ disabled: !hasChanges,
1203
+ },
1204
+ {
1205
+ label: t('actions.newQuestion'),
1206
+ onClick: openCreateSheet,
1207
+ icon: <Plus className="size-4" />,
1208
+ },
1209
+ ]}
684
1210
  />
685
1211
 
686
1212
  <div className="pb-10">
687
- {/* Breadcrumb + actions */}
688
1213
  <motion.div
689
1214
  initial={{ opacity: 0, y: 20 }}
690
1215
  animate={{ opacity: 1, y: 0 }}
691
1216
  transition={{ duration: 0.4 }}
1217
+ className="space-y-6"
692
1218
  >
693
1219
  {/* Stats */}
694
- {!loading && (
695
- <div className="mb-6 grid grid-cols-3 gap-4">
696
- {[
697
- { label: t('stats.questions'), valor: questoes.length },
698
- { label: t('stats.totalScore'), valor: totalPontuacao },
699
- {
700
- label: t('stats.avgAlternatives'),
701
- valor:
702
- questoes.length > 0
703
- ? (
704
- questoes.reduce(
705
- (a, q) => a + q.alternativas.length,
706
- 0
707
- ) / questoes.length
708
- ).toFixed(1)
709
- : '0',
710
- },
711
- ].map((stat, i) => (
712
- <motion.div
713
- key={stat.label}
714
- initial={{ opacity: 0, y: 10 }}
715
- animate={{ opacity: 1, y: 0 }}
716
- transition={{ delay: i * 0.08 }}
717
- >
718
- <Card>
719
- <CardContent className="p-4 text-center">
720
- <p className="text-2xl font-bold">{stat.valor}</p>
721
- <p className="text-xs text-muted-foreground">
722
- {stat.label}
723
- </p>
724
- </CardContent>
725
- </Card>
726
- </motion.div>
1220
+ {loading ? (
1221
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
1222
+ {Array.from({ length: 4 }).map((_, i) => (
1223
+ <Card key={i} className="overflow-hidden border-border/70 py-0">
1224
+ <div className="h-1 w-full bg-linear-to-r from-slate-300/70 via-slate-200 to-transparent" />
1225
+ <CardContent className="p-4">
1226
+ <Skeleton className="mb-2 h-8 w-16" />
1227
+ <Skeleton className="h-4 w-28" />
1228
+ </CardContent>
1229
+ </Card>
727
1230
  ))}
728
1231
  </div>
1232
+ ) : (
1233
+ <KpiCardsGrid items={stats} columns={4} />
729
1234
  )}
730
1235
 
1236
+ <div className="space-y-3">
1237
+ <SearchBar
1238
+ searchQuery={buscaInput}
1239
+ onSearchChange={setBuscaInput}
1240
+ onSearch={() => {
1241
+ setDebouncedBuscaInput(buscaInput.trim());
1242
+ setCurrentPage(1);
1243
+ }}
1244
+ placeholder={t('filters.searchPlaceholder')}
1245
+ controls={[
1246
+ {
1247
+ id: 'tipo',
1248
+ type: 'select',
1249
+ value: filtroTipoInput,
1250
+ onChange: (value) =>
1251
+ setFiltroTipoInput(value as ExamQuestionType | 'todos'),
1252
+ placeholder: t('sheet.fields.questionType'),
1253
+ options: [
1254
+ { value: 'todos', label: t('filters.allTypes') },
1255
+ {
1256
+ value: 'multiple_choice',
1257
+ label: t('question.types.multiple_choice'),
1258
+ },
1259
+ {
1260
+ value: 'true_false',
1261
+ label: t('question.types.true_false'),
1262
+ },
1263
+ { value: 'essay', label: t('question.types.essay') },
1264
+ {
1265
+ value: 'fill_blank',
1266
+ label: t('question.types.fill_blank'),
1267
+ },
1268
+ { value: 'matching', label: t('question.types.matching') },
1269
+ ],
1270
+ },
1271
+ ]}
1272
+ afterSearchButton={
1273
+ <ViewModeToggle
1274
+ viewMode={viewMode}
1275
+ onViewModeChange={setViewMode}
1276
+ listLabel={t('viewMode.list')}
1277
+ cardsLabel={t('viewMode.cards')}
1278
+ />
1279
+ }
1280
+ />
1281
+ <div className="flex flex-wrap items-center justify-between gap-3">
1282
+ <p className="text-sm text-muted-foreground">
1283
+ {filteredQuestoes.length}{' '}
1284
+ {filteredQuestoes.length === 1
1285
+ ? t('pagination.question')
1286
+ : t('pagination.questions')}
1287
+ </p>
1288
+ <div className="flex items-center gap-2">
1289
+ {hasActiveFilters && (
1290
+ <Button
1291
+ type="button"
1292
+ variant="ghost"
1293
+ size="sm"
1294
+ onClick={clearFilters}
1295
+ >
1296
+ {t('filters.clear')}
1297
+ </Button>
1298
+ )}
1299
+ {isFetching && (
1300
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
1301
+ )}
1302
+ </div>
1303
+ </div>
1304
+ </div>
1305
+
731
1306
  {/* Questions list */}
732
1307
  {loading ? (
733
1308
  <div className="flex flex-col gap-4">
734
1309
  {Array.from({ length: 4 }).map((_, i) => (
735
- <Card key={i}>
1310
+ <Card key={i} className="overflow-hidden border-border/70">
736
1311
  <CardContent className="p-4">
737
1312
  <div className="flex items-center gap-3">
738
1313
  <Skeleton className="size-8 rounded-full" />
@@ -745,7 +1320,7 @@ export default function QuestoesPage() {
745
1320
  </Card>
746
1321
  ))}
747
1322
  </div>
748
- ) : questoes.length === 0 ? (
1323
+ ) : filteredQuestoes.length === 0 ? (
749
1324
  <EmptyState
750
1325
  icon={<FileCheck className="h-12 w-12" />}
751
1326
  title={t('empty.title')}
@@ -754,6 +1329,84 @@ export default function QuestoesPage() {
754
1329
  onAction={openCreateSheet}
755
1330
  actionIcon={<Plus className="mr-2 h-4 w-4" />}
756
1331
  />
1332
+ ) : viewMode === 'list' ? (
1333
+ <div className="overflow-hidden rounded-xl border border-border/70">
1334
+ <Table>
1335
+ <TableHeader>
1336
+ <TableRow>
1337
+ <TableHead>{t('table.headers.question')}</TableHead>
1338
+ <TableHead>{t('table.headers.type')}</TableHead>
1339
+ <TableHead className="text-right">
1340
+ {t('table.headers.points')}
1341
+ </TableHead>
1342
+ <TableHead className="w-12" />
1343
+ </TableRow>
1344
+ </TableHeader>
1345
+ <TableBody>
1346
+ {paginatedQuestoes.map((questao) => (
1347
+ <TableRow key={questao.id}>
1348
+ <TableCell>
1349
+ <div className="min-w-0">
1350
+ <p className="truncate font-medium">
1351
+ {stripHtml(questao.enunciado)}
1352
+ </p>
1353
+ <p className="mt-1 text-xs text-muted-foreground">
1354
+ #{questao.order}
1355
+ </p>
1356
+ </div>
1357
+ </TableCell>
1358
+ <TableCell>
1359
+ <Badge variant="outline" className="text-xs">
1360
+ {t(`question.types.${questao.tipo}`)}
1361
+ </Badge>
1362
+ </TableCell>
1363
+ <TableCell className="text-right font-medium">
1364
+ {questao.pontuacao}
1365
+ </TableCell>
1366
+ <TableCell>
1367
+ <DropdownMenu>
1368
+ <DropdownMenuTrigger asChild>
1369
+ <Button
1370
+ variant="ghost"
1371
+ size="icon"
1372
+ className="ml-auto size-8"
1373
+ aria-label={t('table.actions.label')}
1374
+ >
1375
+ <MoreHorizontal className="size-4" />
1376
+ </Button>
1377
+ </DropdownMenuTrigger>
1378
+ <DropdownMenuContent align="end" className="w-48">
1379
+ <DropdownMenuItem
1380
+ onClick={() => openEditSheet(questao)}
1381
+ >
1382
+ <Pencil className="size-3.5" />{' '}
1383
+ {t('question.actions.edit')}
1384
+ </DropdownMenuItem>
1385
+ <DropdownMenuItem
1386
+ onClick={() => duplicateQuestao(questao)}
1387
+ >
1388
+ <Copy className="mr-2 size-4" />{' '}
1389
+ {t('question.actions.duplicate')}
1390
+ </DropdownMenuItem>
1391
+ <DropdownMenuSeparator />
1392
+ <DropdownMenuItem
1393
+ className="text-destructive focus:text-destructive"
1394
+ onClick={() => {
1395
+ setQuestaoToDelete(questao);
1396
+ setDeleteDialogOpen(true);
1397
+ }}
1398
+ >
1399
+ <Trash2 className="size-3.5" />{' '}
1400
+ {t('question.actions.delete')}
1401
+ </DropdownMenuItem>
1402
+ </DropdownMenuContent>
1403
+ </DropdownMenu>
1404
+ </TableCell>
1405
+ </TableRow>
1406
+ ))}
1407
+ </TableBody>
1408
+ </Table>
1409
+ </div>
757
1410
  ) : (
758
1411
  <DndContext
759
1412
  sensors={sensors}
@@ -761,10 +1414,10 @@ export default function QuestoesPage() {
761
1414
  onDragEnd={handleQuestionDragEnd}
762
1415
  >
763
1416
  <SortableContext
764
- items={questoes.map((q) => q.id)}
1417
+ items={paginatedQuestoes.map((q) => q.id)}
765
1418
  strategy={verticalListSortingStrategy}
766
1419
  >
767
- <div className="flex flex-col gap-3">
1420
+ <div className="flex w-full max-w-2xl flex-col gap-3">
768
1421
  <AnimatePresence>
769
1422
  {paginatedQuestoes.map((questao, qIndex) => {
770
1423
  const isExpanded = expandedIds.has(questao.id);
@@ -790,12 +1443,12 @@ export default function QuestoesPage() {
790
1443
  </DndContext>
791
1444
  )}
792
1445
 
793
- {!loading && questoes.length > 0 && (
1446
+ {!loading && filteredQuestoes.length > 0 && (
794
1447
  <div className="mt-6">
795
1448
  <PaginationFooter
796
1449
  currentPage={safePage}
797
1450
  pageSize={pageSize}
798
- totalItems={questoes.length}
1451
+ totalItems={filteredQuestoes.length}
799
1452
  onPageChange={setCurrentPage}
800
1453
  onPageSizeChange={(nextSize) => {
801
1454
  setPageSize(nextSize);
@@ -810,7 +1463,10 @@ export default function QuestoesPage() {
810
1463
 
811
1464
  {/* Create/Edit Sheet */}
812
1465
  <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
813
- <SheetContent className="overflow-y-auto sm:max-w-xl">
1466
+ <SheetContent
1467
+ side="right"
1468
+ className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
1469
+ >
814
1470
  <SheetHeader>
815
1471
  <SheetTitle>
816
1472
  {editingQuestao ? t('sheet.titleEdit') : t('sheet.titleCreate')}
@@ -821,115 +1477,339 @@ export default function QuestoesPage() {
821
1477
  : t('sheet.descriptionCreate')}
822
1478
  </SheetDescription>
823
1479
  </SheetHeader>
824
- <form
825
- onSubmit={form.handleSubmit(onSubmit)}
826
- className="flex flex-col gap-5 px-4 pb-4"
827
- >
828
- <Field data-invalid={!!form.formState.errors.enunciado}>
829
- <FieldLabel htmlFor="enunciado">
830
- {t('sheet.fields.statement')}
831
- </FieldLabel>
832
- <Textarea
833
- id="enunciado"
834
- rows={3}
835
- placeholder={t('sheet.fields.statementPlaceholder')}
836
- {...form.register('enunciado')}
1480
+ <Form {...form}>
1481
+ <form
1482
+ onSubmit={form.handleSubmit(onSubmit)}
1483
+ className="flex flex-1 flex-col gap-5 px-4 pb-4"
1484
+ >
1485
+ <FormField
1486
+ control={form.control}
1487
+ name="enunciado"
1488
+ render={({ field }) => (
1489
+ <FormItem>
1490
+ <FormLabel>{t('sheet.fields.statement')}</FormLabel>
1491
+ <FormControl>
1492
+ <RichTextEditor
1493
+ className="w-full max-w-full h-50"
1494
+ {...field}
1495
+ />
1496
+ </FormControl>
1497
+ <FormMessage />
1498
+ </FormItem>
1499
+ )}
837
1500
  />
838
- {form.formState.errors.enunciado && (
839
- <p className="text-sm text-destructive">
840
- {form.formState.errors.enunciado.message}
841
- </p>
842
- )}
843
- </Field>
844
-
845
- <Field data-invalid={!!form.formState.errors.pontuacao}>
846
- <FieldLabel htmlFor="pontuacao">
847
- {t('sheet.fields.score')}
848
- </FieldLabel>
849
- <Input
850
- id="pontuacao"
851
- type="number"
852
- step="0.5"
853
- placeholder={t('sheet.fields.scorePlaceholder')}
854
- {...form.register('pontuacao')}
1501
+
1502
+ <FormField
1503
+ control={form.control}
1504
+ name="questionType"
1505
+ render={({ field }) => (
1506
+ <FormItem>
1507
+ <FormLabel>{t('sheet.fields.questionType')}</FormLabel>
1508
+ <Select
1509
+ value={field.value}
1510
+ onValueChange={(value) =>
1511
+ field.onChange(value as ExamQuestionType)
1512
+ }
1513
+ >
1514
+ <FormControl>
1515
+ <SelectTrigger className="w-full">
1516
+ <SelectValue
1517
+ placeholder={t(
1518
+ 'sheet.fields.questionTypePlaceholder'
1519
+ )}
1520
+ />
1521
+ </SelectTrigger>
1522
+ </FormControl>
1523
+ <SelectContent>
1524
+ <SelectItem value="multiple_choice">
1525
+ {t('question.types.multiple_choice')}
1526
+ </SelectItem>
1527
+ <SelectItem value="true_false">
1528
+ {t('question.types.true_false')}
1529
+ </SelectItem>
1530
+ <SelectItem value="essay">
1531
+ {t('question.types.essay')}
1532
+ </SelectItem>
1533
+ <SelectItem value="fill_blank">
1534
+ {t('question.types.fill_blank')}
1535
+ </SelectItem>
1536
+ <SelectItem value="matching">
1537
+ {t('question.types.matching')}
1538
+ </SelectItem>
1539
+ </SelectContent>
1540
+ </Select>
1541
+ <FormMessage />
1542
+ </FormItem>
1543
+ )}
855
1544
  />
856
- {form.formState.errors.pontuacao && (
857
- <p className="text-sm text-destructive">
858
- {form.formState.errors.pontuacao.message}
859
- </p>
860
- )}
861
- </Field>
862
1545
 
863
- <Separator />
1546
+ <FormField
1547
+ control={form.control}
1548
+ name="pontuacao"
1549
+ render={({ field }) => (
1550
+ <FormItem>
1551
+ <FormLabel>{t('sheet.fields.score')}</FormLabel>
1552
+ <FormControl>
1553
+ <Input
1554
+ type="number"
1555
+ step="0.5"
1556
+ placeholder={t('sheet.fields.scorePlaceholder')}
1557
+ {...field}
1558
+ />
1559
+ </FormControl>
1560
+ <FormMessage />
1561
+ </FormItem>
1562
+ )}
1563
+ />
864
1564
 
865
- {/* Alternatives with dnd */}
866
- <div>
867
- <div className="mb-3 flex items-center justify-between">
1565
+ <Separator />
1566
+
1567
+ {OBJECTIVE_QUESTION_TYPES.includes(selectedQuestionType) ? (
868
1568
  <div>
869
- <p className="text-sm font-medium">
870
- {t('sheet.fields.alternatives')}
871
- </p>
872
- <p className="text-xs text-muted-foreground">
873
- {t('sheet.fields.alternativesDescription')}
874
- </p>
875
- </div>
876
- <Button
877
- type="button"
878
- variant="outline"
879
- size="sm"
880
- className="gap-1.5"
881
- onClick={addAlternativa}
882
- >
883
- <Plus className="size-3.5" /> {t('sheet.actions.add')}
884
- </Button>
885
- </div>
1569
+ <div className="mb-3 flex items-center justify-between">
1570
+ <div>
1571
+ <p className="text-sm font-medium">
1572
+ {t('sheet.fields.alternatives')}
1573
+ </p>
1574
+ <p className="text-xs text-muted-foreground">
1575
+ {selectedQuestionType === 'true_false'
1576
+ ? t('sheet.fields.trueFalseDescription')
1577
+ : t('sheet.fields.alternativesDescription')}
1578
+ </p>
1579
+ </div>
1580
+ {selectedQuestionType === 'multiple_choice' && (
1581
+ <Button
1582
+ type="button"
1583
+ variant="outline"
1584
+ size="sm"
1585
+ className="gap-1.5"
1586
+ onClick={addAlternativa}
1587
+ >
1588
+ <Plus className="size-3.5" /> {t('sheet.actions.add')}
1589
+ </Button>
1590
+ )}
1591
+ </div>
886
1592
 
887
- <DndContext
888
- sensors={sensors}
889
- collisionDetection={closestCenter}
890
- onDragEnd={handleAltDragEnd}
891
- >
892
- <SortableContext
893
- items={sheetAlternativas.map((a) => a.id)}
894
- strategy={verticalListSortingStrategy}
895
- >
896
- <div className="flex flex-col gap-2">
897
- {sheetAlternativas.map((alt, i) => (
898
- <SortableAlternativa
899
- key={alt.id}
900
- alt={alt}
901
- index={i}
902
- onToggleCorrect={() => toggleCorreta(alt.id)}
903
- onChangeTexto={(t) => updateAlternativaTexto(alt.id, t)}
904
- onRemove={() => removeAlternativa(alt.id)}
905
- canRemove={sheetAlternativas.length > 2}
906
- />
1593
+ <DndContext
1594
+ sensors={sensors}
1595
+ collisionDetection={closestCenter}
1596
+ onDragEnd={handleAltDragEnd}
1597
+ >
1598
+ <SortableContext
1599
+ items={sheetAlternativas.map((a) => a.id)}
1600
+ strategy={verticalListSortingStrategy}
1601
+ >
1602
+ <div className="flex flex-col gap-2">
1603
+ {sheetAlternativas.map((alt, i) => (
1604
+ <SortableAlternativa
1605
+ key={alt.id}
1606
+ alt={alt}
1607
+ index={i}
1608
+ onToggleCorrect={() => toggleCorreta(alt.id)}
1609
+ onChangeTexto={(value) =>
1610
+ updateAlternativaTexto(alt.id, value)
1611
+ }
1612
+ onRemove={() => removeAlternativa(alt.id)}
1613
+ canRemove={
1614
+ selectedQuestionType === 'multiple_choice' &&
1615
+ sheetAlternativas.length > 2
1616
+ }
1617
+ disableTextInput={
1618
+ selectedQuestionType === 'true_false'
1619
+ }
1620
+ />
1621
+ ))}
1622
+ </div>
1623
+ </SortableContext>
1624
+ </DndContext>
1625
+
1626
+ {sheetAlternativas.length < 2 && (
1627
+ <p className="mt-2 text-sm text-destructive">
1628
+ {t('sheet.validation.minAlternatives')}
1629
+ </p>
1630
+ )}
1631
+ </div>
1632
+ ) : selectedQuestionType === 'fill_blank' ? (
1633
+ <div className="space-y-3">
1634
+ <div className="flex items-center justify-between">
1635
+ <div>
1636
+ <p className="text-sm font-medium">
1637
+ {t('sheet.fields.fillBlankAnswers')}
1638
+ </p>
1639
+ <p className="text-xs text-muted-foreground">
1640
+ {t('sheet.fields.fillBlankDescription')}
1641
+ </p>
1642
+ </div>
1643
+ <Button
1644
+ type="button"
1645
+ variant="outline"
1646
+ size="sm"
1647
+ className="gap-1.5"
1648
+ onClick={addFillBlankAnswer}
1649
+ >
1650
+ <Plus className="size-3.5" /> {t('sheet.actions.add')}
1651
+ </Button>
1652
+ </div>
1653
+ <div className="space-y-2">
1654
+ {sheetFillBlankAnswers.map((item, index) => (
1655
+ <div
1656
+ key={item.id}
1657
+ className="space-y-2 rounded-lg border p-3"
1658
+ >
1659
+ <div className="flex items-center justify-between">
1660
+ <p className="text-xs font-medium text-muted-foreground">
1661
+ {t('sheet.fields.fillBlankItem', {
1662
+ number: index + 1,
1663
+ })}
1664
+ </p>
1665
+ {sheetFillBlankAnswers.length > 1 && (
1666
+ <Button
1667
+ type="button"
1668
+ variant="ghost"
1669
+ size="icon"
1670
+ onClick={() => removeFillBlankAnswer(item.id)}
1671
+ >
1672
+ <X className="size-4" />
1673
+ </Button>
1674
+ )}
1675
+ </div>
1676
+ <Input
1677
+ value={item.answer}
1678
+ onChange={(event) =>
1679
+ updateFillBlankAnswer(
1680
+ item.id,
1681
+ 'answer',
1682
+ event.target.value
1683
+ )
1684
+ }
1685
+ placeholder={t(
1686
+ 'sheet.fields.fillBlankExpectedAnswerPlaceholder'
1687
+ )}
1688
+ />
1689
+ <Input
1690
+ value={item.alternativesText}
1691
+ onChange={(event) =>
1692
+ updateFillBlankAnswer(
1693
+ item.id,
1694
+ 'alternativesText',
1695
+ event.target.value
1696
+ )
1697
+ }
1698
+ placeholder={t(
1699
+ 'sheet.fields.fillBlankAlternativeAnswersPlaceholder'
1700
+ )}
1701
+ />
1702
+ </div>
907
1703
  ))}
908
1704
  </div>
909
- </SortableContext>
910
- </DndContext>
1705
+ </div>
1706
+ ) : selectedQuestionType === 'matching' ? (
1707
+ <div className="space-y-3">
1708
+ <div className="flex items-center justify-between">
1709
+ <div>
1710
+ <p className="text-sm font-medium">
1711
+ {t('sheet.fields.matchingPairs')}
1712
+ </p>
1713
+ <p className="text-xs text-muted-foreground">
1714
+ {t('sheet.fields.matchingDescription')}
1715
+ </p>
1716
+ </div>
1717
+ <Button
1718
+ type="button"
1719
+ variant="outline"
1720
+ size="sm"
1721
+ className="gap-1.5"
1722
+ onClick={addMatchingPair}
1723
+ >
1724
+ <Plus className="size-3.5" /> {t('sheet.actions.add')}
1725
+ </Button>
1726
+ </div>
911
1727
 
912
- {sheetAlternativas.length < 2 && (
913
- <p className="mt-2 text-sm text-destructive">
914
- {t('sheet.validation.minAlternatives')}
915
- </p>
1728
+ <DndContext
1729
+ sensors={sensors}
1730
+ collisionDetection={closestCenter}
1731
+ onDragEnd={handleMatchingPairDragEnd}
1732
+ >
1733
+ <SortableContext
1734
+ items={sheetMatchingPairs.map((pair) => pair.id)}
1735
+ strategy={verticalListSortingStrategy}
1736
+ >
1737
+ <div className="space-y-2">
1738
+ {sheetMatchingPairs.map((pair, index) => (
1739
+ <div
1740
+ key={pair.id}
1741
+ className="space-y-2 rounded-lg border p-3"
1742
+ >
1743
+ <div className="flex items-center justify-between">
1744
+ <p className="text-xs font-medium text-muted-foreground">
1745
+ {t('sheet.fields.matchingItem', {
1746
+ number: index + 1,
1747
+ })}
1748
+ </p>
1749
+ {sheetMatchingPairs.length > 2 && (
1750
+ <Button
1751
+ type="button"
1752
+ variant="ghost"
1753
+ size="icon"
1754
+ onClick={() => removeMatchingPair(pair.id)}
1755
+ >
1756
+ <X className="size-4" />
1757
+ </Button>
1758
+ )}
1759
+ </div>
1760
+ <Input
1761
+ value={pair.leftText}
1762
+ onChange={(event) =>
1763
+ updateMatchingPair(
1764
+ pair.id,
1765
+ 'leftText',
1766
+ event.target.value
1767
+ )
1768
+ }
1769
+ placeholder={t(
1770
+ 'sheet.fields.matchingLeftPlaceholder'
1771
+ )}
1772
+ />
1773
+ <Input
1774
+ value={pair.rightText}
1775
+ onChange={(event) =>
1776
+ updateMatchingPair(
1777
+ pair.id,
1778
+ 'rightText',
1779
+ event.target.value
1780
+ )
1781
+ }
1782
+ placeholder={t(
1783
+ 'sheet.fields.matchingRightPlaceholder'
1784
+ )}
1785
+ />
1786
+ </div>
1787
+ ))}
1788
+ </div>
1789
+ </SortableContext>
1790
+ </DndContext>
1791
+ </div>
1792
+ ) : (
1793
+ <div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
1794
+ {t('sheet.fields.essayDescription')}
1795
+ </div>
916
1796
  )}
917
- </div>
918
1797
 
919
- <SheetFooter className="gap-2 p-0 pt-4">
920
- <Button type="submit">
921
- {editingQuestao
922
- ? t('sheet.actions.update')
923
- : t('sheet.actions.create')}
924
- </Button>
925
- </SheetFooter>
926
- </form>
1798
+ <SheetFooter className="mt-auto gap-2 p-0 pt-4">
1799
+ <Button type="submit" className="w-full">
1800
+ {editingQuestao
1801
+ ? t('sheet.actions.update')
1802
+ : t('sheet.actions.create')}
1803
+ </Button>
1804
+ </SheetFooter>
1805
+ </form>
1806
+ </Form>
927
1807
  </SheetContent>
928
1808
  </Sheet>
929
1809
 
930
1810
  {/* Delete Dialog */}
931
1811
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
932
- <DialogContent>
1812
+ <DialogContent className="max-w-3xl">
933
1813
  <DialogHeader>
934
1814
  <DialogTitle className="flex items-center gap-2">
935
1815
  <AlertTriangle className="size-5 text-destructive" />