@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
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { EmptyState, Page, PageHeader } from '@/components/entity-list';
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
4
  import { Badge } from '@/components/ui/badge';
5
5
  import { Button } from '@/components/ui/button';
6
6
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -12,7 +12,10 @@ import {
12
12
  DialogHeader,
13
13
  DialogTitle,
14
14
  } from '@/components/ui/dialog';
15
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
15
16
  import { Progress } from '@/components/ui/progress';
17
+ import { Textarea } from '@/components/ui/textarea';
18
+ import { useApp } from '@hed-hog/next-app-provider';
16
19
  import { AnimatePresence, motion } from 'framer-motion';
17
20
  import {
18
21
  AlertTriangle,
@@ -30,6 +33,7 @@ import {
30
33
  } from 'lucide-react';
31
34
  import { useTranslations } from 'next-intl';
32
35
  import Link from 'next/link';
36
+ import { useParams } from 'next/navigation';
33
37
  import { useEffect, useRef, useState } from 'react';
34
38
  import { toast } from 'sonner';
35
39
 
@@ -214,22 +218,101 @@ const examQuestions: Questao[] = [
214
218
  const EXAM_TITLE = 'Prova Final - React Avancado';
215
219
  const EXAM_TIME_MINUTES = 120;
216
220
 
221
+ interface ExamInfo {
222
+ id: number;
223
+ title: string;
224
+ questionCount: number;
225
+ maxPoints: number;
226
+ timeLimitMinutes: number | null;
227
+ passingScore: number;
228
+ passingPercent: number;
229
+ }
230
+
231
+ interface AttemptsInfo {
232
+ allowed: number;
233
+ used: number;
234
+ remaining: number;
235
+ canStart: boolean;
236
+ }
237
+
238
+ interface AttemptResult {
239
+ totalPoints: number;
240
+ maxPoints: number;
241
+ score: number;
242
+ scoreDisplay: number;
243
+ percent: number;
244
+ passed: boolean | null;
245
+ hasPendingReview: boolean;
246
+ pendingReviewCount: number;
247
+ answersSummary: {
248
+ questionId: number;
249
+ hasAnswer: boolean;
250
+ isCorrect: boolean | null;
251
+ pointsAwarded: number;
252
+ requiresManualReview: boolean;
253
+ }[];
254
+ }
255
+
256
+ type ExamQuestionType =
257
+ | 'multiple_choice'
258
+ | 'true_false'
259
+ | 'essay'
260
+ | 'fill_blank'
261
+ | 'matching';
262
+
263
+ interface ExamQuestion {
264
+ id: number;
265
+ order: number;
266
+ questionType: ExamQuestionType;
267
+ statement: string;
268
+ points: number;
269
+ alternatives: { id: number; text: string }[];
270
+ fillBlankAnswers?: Array<{ answer: string; alternatives?: string[] }>;
271
+ matchingPairs?: Array<{ id: string; leftText: string; rightText: string }>;
272
+ matchingOptions?: Array<{ id: string; text: string }>;
273
+ }
274
+
275
+ type ExamAnswerValue = {
276
+ examOptionId: number | null;
277
+ answerText: string | null;
278
+ matchingPairs: Array<{ leftId: string; rightId: string }>;
279
+ };
280
+
281
+ interface ExamAttemptState {
282
+ exam: ExamInfo;
283
+ attempts: AttemptsInfo;
284
+ questions: ExamQuestion[];
285
+ attempt: {
286
+ id: number;
287
+ status: string;
288
+ timeRemainingSeconds: number | null;
289
+ answeredCount: number;
290
+ answers: {
291
+ questionId: number;
292
+ examOptionId: number | null;
293
+ answerText: string | null;
294
+ matchingPairs: Array<{ leftId: string; rightId: string }>;
295
+ }[];
296
+ result: AttemptResult | null;
297
+ } | null;
298
+ }
299
+
217
300
  export default function TentativaPage() {
218
301
  const t = useTranslations('lms.AttemptPage');
302
+ const { id } = useParams<{ id: string }>();
303
+ const examId = Number(id);
304
+ const { request } = useApp();
305
+ const [examState, setExamState] = useState<ExamAttemptState | null>(null);
306
+ const [isLoadingExam, setIsLoadingExam] = useState(true);
219
307
  const [started, setStarted] = useState(false);
220
308
  const [finished, setFinished] = useState(false);
221
309
  const [currentIndex, setCurrentIndex] = useState(0);
222
- const [answers, setAnswers] = useState<Record<string, string>>({});
310
+ const [answers, setAnswers] = useState<Record<string, ExamAnswerValue>>({});
223
311
  const [flagged, setFlagged] = useState<Set<string>>(new Set());
224
312
  const [timeLeft, setTimeLeft] = useState(EXAM_TIME_MINUTES * 60);
225
313
  const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
226
314
  const [timeUpDialogOpen, setTimeUpDialogOpen] = useState(false);
227
315
  const [showNav, setShowNav] = useState(false);
228
- const [score, setScore] = useState<{
229
- total: number;
230
- max: number;
231
- percent: number;
232
- } | null>(null);
233
316
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
234
317
 
235
318
  // Timer
@@ -255,15 +338,90 @@ export default function TentativaPage() {
255
338
  useEffect(() => {
256
339
  if (started && !finished && Object.keys(answers).length > 0) {
257
340
  const timeout = setTimeout(() => {
258
- // simulate auto-save
341
+ saveAnswers();
259
342
  }, 2000);
260
343
  return () => clearTimeout(timeout);
261
344
  }
262
345
  }, [answers, started, finished]);
263
346
 
264
- const currentQuestion = examQuestions[currentIndex];
265
- const answeredCount = Object.keys(answers).length;
266
- const progressPercent = (answeredCount / examQuestions.length) * 100;
347
+ function isObjectiveQuestion(questionType?: ExamQuestionType) {
348
+ return questionType === 'multiple_choice' || questionType === 'true_false';
349
+ }
350
+
351
+ function hasAnswerValue(answer?: ExamAnswerValue) {
352
+ if (!answer) {
353
+ return false;
354
+ }
355
+
356
+ if (answer.examOptionId != null) {
357
+ return true;
358
+ }
359
+
360
+ if ((answer.answerText ?? '').trim().length > 0) {
361
+ return true;
362
+ }
363
+
364
+ return (answer.matchingPairs ?? []).length > 0;
365
+ }
366
+
367
+ function mapAnswersPayload() {
368
+ return Object.entries(answers)
369
+ .filter(([, answer]) => hasAnswerValue(answer))
370
+ .map(([questionId, answer]) => ({
371
+ questionId: Number(questionId),
372
+ examOptionId: answer.examOptionId,
373
+ answerText: answer.answerText?.trim() || undefined,
374
+ matchingPairs: answer.matchingPairs,
375
+ }));
376
+ }
377
+
378
+ // Load exam state on mount
379
+ useEffect(() => {
380
+ async function load() {
381
+ try {
382
+ const { data } = await request<ExamAttemptState>({
383
+ url: `/lms/exams/${examId}/attempt`,
384
+ method: 'GET',
385
+ });
386
+ setExamState(data);
387
+ if (data.attempt?.timeRemainingSeconds != null) {
388
+ setTimeLeft(data.attempt.timeRemainingSeconds);
389
+ }
390
+ if (data.attempt?.answers) {
391
+ const restoredAnswers: Record<string, ExamAnswerValue> = {};
392
+ for (const answer of data.attempt.answers) {
393
+ restoredAnswers[String(answer.questionId)] = {
394
+ examOptionId: answer.examOptionId,
395
+ answerText: answer.answerText,
396
+ matchingPairs: answer.matchingPairs ?? [],
397
+ };
398
+ }
399
+ setAnswers(restoredAnswers);
400
+ }
401
+ if (data.attempt?.status === 'in_progress') {
402
+ setStarted(true);
403
+ }
404
+ } finally {
405
+ setIsLoadingExam(false);
406
+ }
407
+ }
408
+ load();
409
+ }, [examId]);
410
+
411
+ const apiQuestions = examState?.questions ?? [];
412
+ const currentQuestion = apiQuestions[currentIndex];
413
+ const answeredCount = Object.values(answers).filter((answer) =>
414
+ hasAnswerValue(answer)
415
+ ).length;
416
+ const progressPercent =
417
+ apiQuestions.length > 0 ? (answeredCount / apiQuestions.length) * 100 : 0;
418
+ const hasCompletedAttempt = examState?.attempt?.status === 'completed';
419
+ const hasActiveAttempt = examState?.attempt?.status === 'in_progress';
420
+ const hasRemainingAttempts = examState?.attempts.canStart !== false;
421
+ const shouldShowCompletedNotice =
422
+ hasCompletedAttempt &&
423
+ !hasRemainingAttempts &&
424
+ Boolean(examState?.attempt?.result);
267
425
 
268
426
  if (!currentQuestion) {
269
427
  return (
@@ -298,7 +456,47 @@ export default function TentativaPage() {
298
456
  }
299
457
 
300
458
  function selectAnswer(questionId: string, alternativaId: string) {
301
- setAnswers((prev) => ({ ...prev, [questionId]: alternativaId }));
459
+ setAnswers((prev) => ({
460
+ ...prev,
461
+ [questionId]: {
462
+ examOptionId: Number(alternativaId),
463
+ answerText: null,
464
+ matchingPairs: [],
465
+ },
466
+ }));
467
+ }
468
+
469
+ function updateTextAnswer(questionId: string, answerText: string) {
470
+ setAnswers((prev) => ({
471
+ ...prev,
472
+ [questionId]: {
473
+ examOptionId: null,
474
+ answerText,
475
+ matchingPairs: [],
476
+ },
477
+ }));
478
+ }
479
+
480
+ function updateMatchingAnswer(
481
+ questionId: string,
482
+ leftId: string,
483
+ rightId: string
484
+ ) {
485
+ setAnswers((prev) => {
486
+ const currentPairs = prev[questionId]?.matchingPairs ?? [];
487
+ const nextPairs = currentPairs
488
+ .filter((pair) => pair.leftId !== leftId)
489
+ .concat(rightId ? [{ leftId, rightId }] : []);
490
+
491
+ return {
492
+ ...prev,
493
+ [questionId]: {
494
+ examOptionId: null,
495
+ answerText: null,
496
+ matchingPairs: nextPairs,
497
+ },
498
+ };
499
+ });
302
500
  }
303
501
 
304
502
  function toggleFlag(questionId: string) {
@@ -315,35 +513,104 @@ export default function TentativaPage() {
315
513
  setShowNav(false);
316
514
  }
317
515
 
318
- function handleSaveProgress() {
319
- toast.success(t('examInProgress.progressSaved'));
516
+ async function saveAnswers() {
517
+ const attemptId = examState?.attempt?.id;
518
+ if (!attemptId) return;
519
+ await request({
520
+ url: `/lms/exams/${examId}/attempt/${attemptId}/answers`,
521
+ method: 'PATCH',
522
+ data: {
523
+ answers: mapAnswersPayload(),
524
+ },
525
+ });
320
526
  }
321
527
 
322
- function calculateScore() {
323
- let total = 0;
324
- let max = 0;
325
- examQuestions.forEach((q) => {
326
- max += q.pontuacao;
327
- const selectedAltId = answers[q.id];
328
- if (selectedAltId) {
329
- const alt = q.alternativas.find((a) => a.id === selectedAltId);
330
- if (alt?.correta) total += q.pontuacao;
331
- }
332
- });
333
- return { total, max, percent: Math.round((total / max) * 100) };
528
+ async function handleSaveProgress() {
529
+ await saveAnswers();
530
+ toast.success(t('examInProgress.progressSaved'));
334
531
  }
335
532
 
336
- function handleSubmit() {
533
+ async function handleSubmit(force = false) {
337
534
  if (timerRef.current) clearInterval(timerRef.current);
338
- const result = calculateScore();
339
- setScore(result);
340
- setFinished(true);
341
535
  setSubmitDialogOpen(false);
342
536
  setTimeUpDialogOpen(false);
537
+ try {
538
+ const attemptId = examState?.attempt?.id;
539
+ if (!attemptId) return;
540
+ const { data } = await request<ExamAttemptState>({
541
+ url: `/lms/exams/${examId}/attempt/${attemptId}/submit`,
542
+ method: 'POST',
543
+ data: {
544
+ force,
545
+ answers: mapAnswersPayload(),
546
+ },
547
+ });
548
+ setExamState(data);
549
+ setFinished(true);
550
+ } catch {
551
+ toast.error(t('examInProgress.submitError'));
552
+ }
343
553
  }
344
554
 
345
555
  function handleForceSubmit() {
346
- handleSubmit();
556
+ handleSubmit(true);
557
+ }
558
+
559
+ async function handleStartAttempt() {
560
+ const { data } = await request<ExamAttemptState>({
561
+ url: `/lms/exams/${examId}/attempt/start`,
562
+ method: 'POST',
563
+ data: {},
564
+ });
565
+ setExamState(data);
566
+ if (data.attempt?.timeRemainingSeconds != null) {
567
+ setTimeLeft(data.attempt.timeRemainingSeconds);
568
+ }
569
+ if (data.attempt?.answers) {
570
+ const restoredAnswers: Record<string, ExamAnswerValue> = {};
571
+ for (const a of data.attempt.answers) {
572
+ restoredAnswers[String(a.questionId)] = {
573
+ examOptionId: a.examOptionId,
574
+ answerText: a.answerText,
575
+ matchingPairs: a.matchingPairs ?? [],
576
+ };
577
+ }
578
+ setAnswers(restoredAnswers);
579
+ }
580
+ setStarted(true);
581
+ }
582
+
583
+ // Loading screen
584
+ if (isLoadingExam) {
585
+ return (
586
+ <Page>
587
+ <PageHeader
588
+ title={t('breadcrumbs.attemptExam')}
589
+ breadcrumbs={[
590
+ { label: t('breadcrumbs.home'), href: '/' },
591
+ { label: t('breadcrumbs.exams'), href: '/lms/exams' },
592
+ { label: t('breadcrumbs.attemptExam') },
593
+ ]}
594
+ />
595
+ <div className="flex justify-center">
596
+ <Card className="w-full max-w-3xl animate-pulse border-border/70">
597
+ <CardHeader className="text-center">
598
+ <div className="mx-auto mb-4 size-16 rounded-2xl bg-muted" />
599
+ <div className="mx-auto h-7 w-48 rounded bg-muted" />
600
+ </CardHeader>
601
+ <CardContent className="flex flex-col gap-4">
602
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
603
+ {[...Array(4)].map((_, i) => (
604
+ <div key={i} className="h-16 rounded-lg bg-muted" />
605
+ ))}
606
+ </div>
607
+ <div className="h-32 rounded-lg bg-muted" />
608
+ <div className="h-10 rounded bg-muted" />
609
+ </CardContent>
610
+ </Card>
611
+ </div>
612
+ </Page>
613
+ );
347
614
  }
348
615
 
349
616
  // Start screen
@@ -351,6 +618,8 @@ export default function TentativaPage() {
351
618
  return (
352
619
  <Page>
353
620
  <PageHeader
621
+ title={examState?.exam.title ?? EXAM_TITLE}
622
+ description={t('breadcrumbs.attemptExam')}
354
623
  breadcrumbs={[
355
624
  {
356
625
  label: t('breadcrumbs.home'),
@@ -370,45 +639,71 @@ export default function TentativaPage() {
370
639
  initial={{ opacity: 0, y: 20 }}
371
640
  animate={{ opacity: 1, y: 0 }}
372
641
  transition={{ duration: 0.5 }}
642
+ className="w-full max-w-4xl"
373
643
  >
374
- <Card className="w-full max-w-lg">
644
+ <Card className="border-border/70">
375
645
  <CardHeader className="text-center">
376
- <div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl bg-orange-500">
377
- <GraduationCap className="size-8 text-background" />
646
+ <div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl bg-orange-100 text-orange-700">
647
+ <GraduationCap className="size-8" />
378
648
  </div>
379
- <CardTitle className="text-2xl">{EXAM_TITLE}</CardTitle>
649
+ <CardTitle className="text-2xl">
650
+ {examState?.exam.title ?? EXAM_TITLE}
651
+ </CardTitle>
380
652
  </CardHeader>
381
- <CardContent className="flex flex-col gap-4">
382
- <div className="grid grid-cols-2 gap-3">
383
- <div className="rounded-lg bg-muted p-3 text-center">
384
- <p className="text-2xl font-bold">{examQuestions.length}</p>
385
- <p className="text-xs text-muted-foreground">
386
- {t('startScreen.questions')}
387
- </p>
388
- </div>
389
- <div className="rounded-lg bg-muted p-3 text-center">
390
- <p className="text-2xl font-bold">{EXAM_TIME_MINUTES}</p>
391
- <p className="text-xs text-muted-foreground">
392
- {t('startScreen.minutes')}
393
- </p>
394
- </div>
395
- <div className="rounded-lg bg-muted p-3 text-center">
396
- <p className="text-2xl font-bold">
397
- {examQuestions.reduce((a, q) => a + q.pontuacao, 0)}
398
- </p>
399
- <p className="text-xs text-muted-foreground">
400
- {t('startScreen.points')}
401
- </p>
402
- </div>
403
- <div className="rounded-lg bg-muted p-3 text-center">
404
- <p className="text-2xl font-bold">7.0</p>
405
- <p className="text-xs text-muted-foreground">
406
- {t('startScreen.minGrade')}
407
- </p>
408
- </div>
409
- </div>
653
+ <CardContent className="flex flex-col gap-5">
654
+ <KpiCardsGrid
655
+ items={[
656
+ {
657
+ key: 'questions',
658
+ title: t('startScreen.questions'),
659
+ value:
660
+ examState?.exam.questionCount ?? examQuestions.length,
661
+ icon: GraduationCap,
662
+ layout: 'compact',
663
+ accentClassName:
664
+ 'from-orange-500/25 via-amber-500/15 to-transparent',
665
+ iconContainerClassName: 'bg-orange-100 text-orange-700',
666
+ },
667
+ {
668
+ key: 'minutes',
669
+ title: t('startScreen.minutes'),
670
+ value:
671
+ examState?.exam.timeLimitMinutes ?? EXAM_TIME_MINUTES,
672
+ icon: Clock,
673
+ layout: 'compact',
674
+ accentClassName:
675
+ 'from-sky-500/25 via-blue-500/15 to-transparent',
676
+ iconContainerClassName: 'bg-sky-100 text-sky-700',
677
+ },
678
+ {
679
+ key: 'points',
680
+ title: t('startScreen.points'),
681
+ value:
682
+ examState?.exam.maxPoints ??
683
+ examQuestions.reduce((a, q) => a + q.pontuacao, 0),
684
+ icon: CheckCircle2,
685
+ layout: 'compact',
686
+ accentClassName:
687
+ 'from-emerald-500/25 via-green-500/15 to-transparent',
688
+ iconContainerClassName: 'bg-emerald-100 text-emerald-700',
689
+ },
690
+ {
691
+ key: 'min-grade',
692
+ title: t('startScreen.minGrade'),
693
+ value:
694
+ examState != null
695
+ ? examState.exam.passingScore.toFixed(1)
696
+ : '7.0',
697
+ icon: BookmarkCheck,
698
+ layout: 'compact',
699
+ accentClassName:
700
+ 'from-violet-500/25 via-indigo-500/15 to-transparent',
701
+ iconContainerClassName: 'bg-violet-100 text-violet-700',
702
+ },
703
+ ]}
704
+ />
410
705
 
411
- <div className="rounded-lg border p-4">
706
+ <div className="rounded-xl border border-border/70 p-5">
412
707
  <h3 className="mb-2 text-sm font-medium">
413
708
  {t('startScreen.instructions')}
414
709
  </h3>
@@ -421,14 +716,39 @@ export default function TentativaPage() {
421
716
  </ul>
422
717
  </div>
423
718
 
424
- <div className="flex gap-3">
425
- <Button
426
- className="flex-1 gap-2"
427
- onClick={() => setStarted(true)}
428
- >
429
- {t('startScreen.startButton')}
430
- </Button>
431
- </div>
719
+ {shouldShowCompletedNotice ? (
720
+ <div className="rounded-xl border border-primary/20 bg-primary/5 p-5">
721
+ <p className="text-sm font-semibold text-primary">
722
+ VOCE JA REALIZOU ESSE TESTE
723
+ </p>
724
+ <p className="mt-1 text-sm text-muted-foreground">
725
+ Sua tentativa ja foi concluida e nao ha novas tentativas
726
+ disponiveis.
727
+ </p>
728
+ <div className="mt-4 flex gap-3">
729
+ <Button
730
+ className="flex-1 gap-2"
731
+ onClick={() => {
732
+ setStarted(true);
733
+ setFinished(true);
734
+ }}
735
+ >
736
+ VEJA SEU RESULTADO
737
+ </Button>
738
+ </div>
739
+ </div>
740
+ ) : (
741
+ <div className="flex gap-3">
742
+ <Button
743
+ className="flex-1 gap-2"
744
+ onClick={handleStartAttempt}
745
+ >
746
+ {hasActiveAttempt
747
+ ? 'Continuar teste'
748
+ : t('startScreen.startButton')}
749
+ </Button>
750
+ </div>
751
+ )}
432
752
  </CardContent>
433
753
  </Card>
434
754
  </motion.div>
@@ -438,11 +758,17 @@ export default function TentativaPage() {
438
758
  }
439
759
 
440
760
  // Results screen
441
- if (finished && score) {
442
- const passed = score.percent >= 70;
761
+ const result = examState?.attempt?.result;
762
+ if (finished && result) {
763
+ const passed = result.passed === true;
764
+ const apiAnsweredCount = examState?.attempt?.answeredCount ?? 0;
765
+ const totalQuestions = examState?.exam.questionCount ?? 0;
766
+ const questions = examState?.questions ?? [];
443
767
  return (
444
768
  <Page>
445
769
  <PageHeader
770
+ title={examState?.exam.title ?? EXAM_TITLE}
771
+ description={t('breadcrumbs.examResult')}
446
772
  breadcrumbs={[
447
773
  {
448
774
  label: t('breadcrumbs.home'),
@@ -463,8 +789,9 @@ export default function TentativaPage() {
463
789
  initial={{ opacity: 0, scale: 0.95 }}
464
790
  animate={{ opacity: 1, scale: 1 }}
465
791
  transition={{ duration: 0.5 }}
792
+ className="w-full max-w-4xl"
466
793
  >
467
- <Card className="w-full max-w-lg">
794
+ <Card className="border-border/70">
468
795
  <CardHeader className="text-center">
469
796
  <motion.div
470
797
  initial={{ scale: 0 }}
@@ -479,13 +806,17 @@ export default function TentativaPage() {
479
806
  )}
480
807
  </motion.div>
481
808
  <CardTitle className="text-2xl">
482
- {passed ? t('resultScreen.passed') : t('resultScreen.failed')}
809
+ {result.hasPendingReview
810
+ ? t('resultScreen.pendingReview')
811
+ : passed
812
+ ? t('resultScreen.passed')
813
+ : t('resultScreen.failed')}
483
814
  </CardTitle>
484
815
  <p className="mt-1 text-sm text-muted-foreground">
485
- {EXAM_TITLE}
816
+ {examState?.exam.title ?? EXAM_TITLE}
486
817
  </p>
487
818
  </CardHeader>
488
- <CardContent className="flex flex-col gap-4">
819
+ <CardContent className="flex flex-col gap-5">
489
820
  <div className="text-center">
490
821
  <motion.p
491
822
  initial={{ opacity: 0 }}
@@ -493,63 +824,78 @@ export default function TentativaPage() {
493
824
  transition={{ delay: 0.4 }}
494
825
  className="text-5xl font-bold"
495
826
  >
496
- {score.percent}%
827
+ {result.hasPendingReview
828
+ ? t('resultScreen.pendingBadge')
829
+ : `${result.percent}%`}
497
830
  </motion.p>
498
831
  <p className="mt-1 text-sm text-muted-foreground">
499
- {t('resultScreen.scoreText', {
500
- total: score.total,
501
- max: score.max,
502
- })}
832
+ {result.hasPendingReview
833
+ ? t('resultScreen.pendingDescription', {
834
+ count: result.pendingReviewCount,
835
+ })
836
+ : t('resultScreen.scoreText', {
837
+ total: result.totalPoints,
838
+ max: result.maxPoints,
839
+ })}
503
840
  </p>
504
841
  </div>
505
842
 
506
- <div className="grid grid-cols-3 gap-3">
507
- <div className="rounded-lg bg-muted p-3 text-center">
508
- <p className="text-lg font-bold">{answeredCount}</p>
509
- <p className="text-xs text-muted-foreground">
510
- {t('resultScreen.answered')}
511
- </p>
512
- </div>
513
- <div className="rounded-lg bg-muted p-3 text-center">
514
- <p className="text-lg font-bold">
515
- {examQuestions.length - answeredCount}
516
- </p>
517
- <p className="text-xs text-muted-foreground">
518
- {t('resultScreen.blank')}
519
- </p>
520
- </div>
521
- <div className="rounded-lg bg-muted p-3 text-center">
522
- <p className="text-lg font-bold">
523
- {formatTime(EXAM_TIME_MINUTES * 60 - timeLeft)}
524
- </p>
525
- <p className="text-xs text-muted-foreground">
526
- {t('resultScreen.timeUsed')}
527
- </p>
528
- </div>
529
- </div>
843
+ <KpiCardsGrid
844
+ items={[
845
+ {
846
+ key: 'answered',
847
+ title: t('resultScreen.answered'),
848
+ value: apiAnsweredCount,
849
+ icon: CheckCircle2,
850
+ layout: 'compact',
851
+ accentClassName:
852
+ 'from-emerald-500/25 via-green-500/15 to-transparent',
853
+ iconContainerClassName: 'bg-emerald-100 text-emerald-700',
854
+ },
855
+ {
856
+ key: 'blank',
857
+ title: t('resultScreen.blank'),
858
+ value: totalQuestions - apiAnsweredCount,
859
+ icon: X,
860
+ layout: 'compact',
861
+ accentClassName:
862
+ 'from-slate-500/25 via-zinc-500/15 to-transparent',
863
+ iconContainerClassName: 'bg-slate-100 text-slate-700',
864
+ },
865
+ {
866
+ key: 'time-used',
867
+ title: t('resultScreen.timeUsed'),
868
+ value: formatTime(EXAM_TIME_MINUTES * 60 - timeLeft),
869
+ icon: Clock,
870
+ layout: 'compact',
871
+ accentClassName:
872
+ 'from-sky-500/25 via-blue-500/15 to-transparent',
873
+ iconContainerClassName: 'bg-sky-100 text-sky-700',
874
+ },
875
+ ]}
876
+ columns={3}
877
+ />
530
878
 
531
879
  {/* Review answers */}
532
- <div className="h-auto rounded-lg border p-3">
880
+ <div className="h-auto rounded-xl border border-border/70 p-4">
533
881
  <p className="mb-2 text-sm font-medium">
534
882
  {t('resultScreen.answersSummary')}
535
883
  </p>
536
884
  <div className="flex flex-col gap-2">
537
- {examQuestions.map((q, i) => {
538
- const selectedAltId = answers[q.id];
539
- const selectedAlt = q.alternativas.find(
540
- (a) => a.id === selectedAltId
885
+ {result.answersSummary.map((summary, i) => {
886
+ const q = questions.find(
887
+ (q) => q.id === summary.questionId
541
888
  );
542
- const isCorrect = selectedAlt?.correta;
543
889
  return (
544
890
  <div
545
- key={q.id}
891
+ key={summary.questionId}
546
892
  className="flex items-center gap-2 text-sm"
547
893
  >
548
894
  <span
549
895
  className={`flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium ${
550
- !selectedAltId
896
+ !summary.hasAnswer
551
897
  ? 'bg-muted text-muted-foreground'
552
- : isCorrect
898
+ : summary.isCorrect
553
899
  ? 'bg-primary text-primary-foreground'
554
900
  : 'bg-destructive/10 text-destructive'
555
901
  }`}
@@ -557,14 +903,18 @@ export default function TentativaPage() {
557
903
  {i + 1}
558
904
  </span>
559
905
  <span className="flex-1 truncate text-muted-foreground">
560
- {q.enunciado.substring(0, 50)}...
906
+ {q
907
+ ? `${q.statement.substring(0, 50)}...`
908
+ : `#${summary.questionId}`}
561
909
  </span>
562
910
  <span className="shrink-0 text-xs font-medium">
563
- {!selectedAltId
911
+ {!summary.hasAnswer
564
912
  ? t('resultScreen.noAnswer')
565
- : isCorrect
566
- ? `+${q.pontuacao}`
567
- : '0'}
913
+ : summary.requiresManualReview
914
+ ? t('resultScreen.pendingAnswer')
915
+ : summary.isCorrect
916
+ ? `+${summary.pointsAwarded}`
917
+ : '0'}
568
918
  </span>
569
919
  </div>
570
920
  );
@@ -591,6 +941,11 @@ export default function TentativaPage() {
591
941
  return (
592
942
  <Page>
593
943
  <PageHeader
944
+ title={examState?.exam.title ?? EXAM_TITLE}
945
+ description={t('examInProgress.answeredCount', {
946
+ count: answeredCount,
947
+ total: apiQuestions.length,
948
+ })}
594
949
  breadcrumbs={[
595
950
  {
596
951
  label: t('breadcrumbs.home'),
@@ -643,38 +998,45 @@ export default function TentativaPage() {
643
998
  }
644
999
  />
645
1000
 
646
- {/* Progress bar */}
647
- <div className="px-4 pb-2">
648
- <Progress value={progressPercent} className="h-1.5" />
649
- </div>
650
-
651
- {/* Top bar */}
652
- <header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur">
653
- <div className="flex h-14 items-center justify-between px-4">
654
- <div className="flex items-center gap-3">
1001
+ <Card className="mb-6 border-border/70">
1002
+ <CardContent className="space-y-4 p-4">
1003
+ <div className="flex flex-wrap items-center justify-between gap-3">
655
1004
  <div>
656
- <p className="text-sm font-medium leading-none">{EXAM_TITLE}</p>
657
- <p className="mt-0.5 text-xs text-muted-foreground">
1005
+ <p className="text-sm font-medium">
658
1006
  {t('examInProgress.question')} {currentIndex + 1}{' '}
659
- {t('examInProgress.of')} {examQuestions.length}
1007
+ {t('examInProgress.of')} {apiQuestions.length}
1008
+ </p>
1009
+ <p className="text-xs text-muted-foreground">
1010
+ {currentQuestion?.questionType
1011
+ ? t(`examInProgress.types.${currentQuestion.questionType}`)
1012
+ : ''}
660
1013
  </p>
661
1014
  </div>
1015
+ <div className="flex items-center gap-2">
1016
+ <Badge variant="outline" className="font-mono">
1017
+ {formatTime(timeLeft)}
1018
+ </Badge>
1019
+ <Badge variant="secondary">
1020
+ {answeredCount}/{apiQuestions.length}
1021
+ </Badge>
1022
+ </div>
662
1023
  </div>
663
- </div>
664
- </header>
1024
+ <Progress value={progressPercent} className="h-1.5" />
1025
+ </CardContent>
1026
+ </Card>
665
1027
 
666
1028
  <div className="flex gap-6">
667
1029
  {/* Question area */}
668
1030
  <div className="flex-1">
669
1031
  <AnimatePresence mode="wait">
670
1032
  <motion.div
671
- key={currentQuestion.id}
1033
+ key={currentQuestion?.id ?? currentIndex}
672
1034
  initial={{ opacity: 0, x: 20 }}
673
1035
  animate={{ opacity: 1, x: 0 }}
674
1036
  exit={{ opacity: 0, x: -20 }}
675
1037
  transition={{ duration: 0.25 }}
676
1038
  >
677
- <Card>
1039
+ <Card className="border-border/70">
678
1040
  <CardContent className="p-6">
679
1041
  {/* Question header */}
680
1042
  <div className="mb-6 flex items-start justify-between gap-4">
@@ -682,16 +1044,23 @@ export default function TentativaPage() {
682
1044
  <div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary text-base font-bold text-primary-foreground">
683
1045
  {currentIndex + 1}
684
1046
  </div>
685
- <div>
1047
+ <div className="space-y-2">
686
1048
  <p className="text-base font-medium leading-relaxed">
687
- {currentQuestion.enunciado}
1049
+ {currentQuestion?.statement}
688
1050
  </p>
689
1051
  <div className="mt-2 flex items-center gap-2">
690
1052
  <Badge variant="outline" className="text-xs">
691
- {currentQuestion.pontuacao} pt
692
- {currentQuestion.pontuacao !== 1 ? 's' : ''}
1053
+ {currentQuestion?.points} pt
1054
+ {currentQuestion?.points !== 1 ? 's' : ''}
693
1055
  </Badge>
694
- {flagged.has(currentQuestion.id) && (
1056
+ {currentQuestion?.questionType && (
1057
+ <Badge variant="secondary" className="text-xs">
1058
+ {t(
1059
+ `examInProgress.types.${currentQuestion.questionType}`
1060
+ )}
1061
+ </Badge>
1062
+ )}
1063
+ {flagged.has(String(currentQuestion?.id)) && (
695
1064
  <Badge variant="secondary" className="text-xs">
696
1065
  {t('examInProgress.marked')}
697
1066
  </Badge>
@@ -702,51 +1071,128 @@ export default function TentativaPage() {
702
1071
  <Button
703
1072
  variant="ghost"
704
1073
  size="icon"
705
- className={`shrink-0 ${flagged.has(currentQuestion.id) ? 'text-primary' : 'text-muted-foreground'}`}
706
- onClick={() => toggleFlag(currentQuestion.id)}
1074
+ className={`shrink-0 ${flagged.has(String(currentQuestion?.id)) ? 'text-primary' : 'text-muted-foreground'}`}
1075
+ onClick={() => toggleFlag(String(currentQuestion?.id))}
707
1076
  aria-label={t('examInProgress.markQuestion')}
708
1077
  >
709
1078
  <Flag
710
- className={`size-5 ${flagged.has(currentQuestion.id) ? 'fill-current' : ''}`}
1079
+ className={`size-5 ${flagged.has(String(currentQuestion?.id)) ? 'fill-current' : ''}`}
711
1080
  />
712
1081
  </Button>
713
1082
  </div>
714
1083
 
715
1084
  {/* Alternatives */}
716
- <div className="flex flex-col gap-3">
717
- {currentQuestion.alternativas.map((alt, i) => {
718
- const isSelected = answers[currentQuestion.id] === alt.id;
719
- return (
720
- <motion.button
721
- key={alt.id}
722
- whileHover={{ scale: 1.01 }}
723
- whileTap={{ scale: 0.99 }}
724
- onClick={() =>
725
- selectAnswer(currentQuestion.id, alt.id)
726
- }
727
- className={`flex items-center gap-3 rounded-xl border p-4 text-left text-sm transition-all ${
728
- isSelected
729
- ? 'border-primary bg-primary/5 font-medium'
730
- : 'border-border hover:border-primary/30 hover:bg-muted/50'
731
- }`}
732
- >
733
- <span
734
- className={`flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium transition-colors ${
1085
+ {isObjectiveQuestion(currentQuestion?.questionType) ? (
1086
+ <div className="flex flex-col gap-3">
1087
+ {(currentQuestion?.alternatives ?? []).map((alt, i) => {
1088
+ const isSelected =
1089
+ answers[String(currentQuestion?.id)]?.examOptionId ===
1090
+ alt.id;
1091
+ return (
1092
+ <motion.button
1093
+ key={alt.id}
1094
+ whileHover={{ scale: 1.01 }}
1095
+ whileTap={{ scale: 0.99 }}
1096
+ onClick={() =>
1097
+ selectAnswer(
1098
+ String(currentQuestion?.id),
1099
+ String(alt.id)
1100
+ )
1101
+ }
1102
+ className={`flex items-center gap-3 rounded-xl border p-4 text-left text-sm transition-all ${
735
1103
  isSelected
736
- ? 'border-primary bg-primary text-primary-foreground'
737
- : 'border-border'
1104
+ ? 'border-primary bg-primary/5 font-medium'
1105
+ : 'border-border hover:border-primary/30 hover:bg-muted/50'
738
1106
  }`}
739
1107
  >
740
- {String.fromCharCode(65 + i)}
741
- </span>
742
- <span className="flex-1">{alt.texto}</span>
743
- {isSelected && (
744
- <CircleDot className="size-5 shrink-0" />
745
- )}
746
- </motion.button>
747
- );
748
- })}
749
- </div>
1108
+ <span
1109
+ className={`flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium transition-colors ${
1110
+ isSelected
1111
+ ? 'border-primary bg-primary text-primary-foreground'
1112
+ : 'border-border'
1113
+ }`}
1114
+ >
1115
+ {String.fromCharCode(65 + i)}
1116
+ </span>
1117
+ <span className="flex-1">{alt.text}</span>
1118
+ {isSelected && (
1119
+ <CircleDot className="size-5 shrink-0" />
1120
+ )}
1121
+ </motion.button>
1122
+ );
1123
+ })}
1124
+ </div>
1125
+ ) : currentQuestion?.questionType === 'matching' ? (
1126
+ <div className="space-y-3">
1127
+ <p className="text-sm text-muted-foreground">
1128
+ {t('examInProgress.matchingDescription')}
1129
+ </p>
1130
+ {(currentQuestion?.matchingPairs ?? []).map((pair) => {
1131
+ const selectedRightId = (
1132
+ answers[String(currentQuestion?.id)]?.matchingPairs ??
1133
+ []
1134
+ ).find((item) => item.leftId === pair.id)?.rightId;
1135
+
1136
+ return (
1137
+ <div
1138
+ key={pair.id}
1139
+ className="grid gap-2 rounded-lg border p-3 md:grid-cols-2"
1140
+ >
1141
+ <p className="text-sm font-medium">
1142
+ {pair.leftText}
1143
+ </p>
1144
+ <select
1145
+ value={selectedRightId ?? ''}
1146
+ onChange={(event) =>
1147
+ updateMatchingAnswer(
1148
+ String(currentQuestion?.id),
1149
+ pair.id,
1150
+ event.target.value
1151
+ )
1152
+ }
1153
+ className="h-9 rounded-md border bg-background px-3 text-sm"
1154
+ >
1155
+ <option value="">
1156
+ {t('examInProgress.matchingSelectPlaceholder')}
1157
+ </option>
1158
+ {(currentQuestion?.matchingOptions ?? []).map(
1159
+ (option) => (
1160
+ <option key={option.id} value={option.id}>
1161
+ {option.text}
1162
+ </option>
1163
+ )
1164
+ )}
1165
+ </select>
1166
+ </div>
1167
+ );
1168
+ })}
1169
+ </div>
1170
+ ) : (
1171
+ <div className="space-y-3">
1172
+ <p className="text-sm text-muted-foreground">
1173
+ {currentQuestion?.questionType === 'fill_blank'
1174
+ ? t('examInProgress.fillBlankDescription')
1175
+ : t('examInProgress.essayDescription')}
1176
+ </p>
1177
+ <Textarea
1178
+ rows={10}
1179
+ value={
1180
+ answers[String(currentQuestion?.id)]?.answerText ?? ''
1181
+ }
1182
+ onChange={(event) =>
1183
+ updateTextAnswer(
1184
+ String(currentQuestion?.id),
1185
+ event.target.value
1186
+ )
1187
+ }
1188
+ placeholder={
1189
+ currentQuestion?.questionType === 'fill_blank'
1190
+ ? t('examInProgress.fillBlankPlaceholder')
1191
+ : t('examInProgress.essayPlaceholder')
1192
+ }
1193
+ />
1194
+ </div>
1195
+ )}
750
1196
 
751
1197
  {/* Navigation */}
752
1198
  <div className="mt-8 flex items-center justify-between">
@@ -761,12 +1207,12 @@ export default function TentativaPage() {
761
1207
  </Button>
762
1208
  <Button
763
1209
  variant="ghost"
764
- className={`shrink-0 lg:hidden ${flagged.has(currentQuestion.id) ? 'text-primary' : 'text-muted-foreground'}`}
1210
+ className="lg:hidden"
765
1211
  onClick={() => setShowNav(!showNav)}
766
1212
  >
767
- {currentIndex + 1}/{examQuestions.length}
1213
+ {currentIndex + 1}/{apiQuestions.length}
768
1214
  </Button>
769
- {currentIndex < examQuestions.length - 1 ? (
1215
+ {currentIndex < apiQuestions.length - 1 ? (
770
1216
  <Button
771
1217
  className="gap-2"
772
1218
  onClick={() => setCurrentIndex((p) => p + 1)}
@@ -792,7 +1238,7 @@ export default function TentativaPage() {
792
1238
 
793
1239
  {/* Side navigation - desktop */}
794
1240
  <div className="hidden w-64 shrink-0 lg:block">
795
- <Card className="sticky top-20">
1241
+ <Card className="sticky top-20 border-border/70">
796
1242
  <CardHeader className="pb-3">
797
1243
  <CardTitle className="text-sm font-medium">
798
1244
  {t('examInProgress.navigation')}
@@ -800,15 +1246,15 @@ export default function TentativaPage() {
800
1246
  <p className="text-xs text-muted-foreground">
801
1247
  {t('examInProgress.answeredCount', {
802
1248
  count: answeredCount,
803
- total: examQuestions.length,
1249
+ total: apiQuestions.length,
804
1250
  })}
805
1251
  </p>
806
1252
  </CardHeader>
807
1253
  <CardContent className="pb-4">
808
1254
  <div className="grid grid-cols-5 gap-2">
809
- {examQuestions.map((q, i) => {
810
- const isAnswered = !!answers[q.id];
811
- const isFlagged = flagged.has(q.id);
1255
+ {apiQuestions.map((q, i) => {
1256
+ const isAnswered = hasAnswerValue(answers[String(q.id)]);
1257
+ const isFlagged = flagged.has(String(q.id));
812
1258
  const isCurrent = i === currentIndex;
813
1259
  return (
814
1260
  <button
@@ -819,7 +1265,7 @@ export default function TentativaPage() {
819
1265
  ? 'bg-primary text-primary-foreground ring-1 ring-primary ring-offset-2'
820
1266
  : isAnswered
821
1267
  ? 'bg-primary/10 text-primary'
822
- : 'bg-muted text-muted-foreground hover:bg-muted/80'
1268
+ : 'bg-muted/80 text-muted-foreground hover:bg-muted'
823
1269
  }`}
824
1270
  aria-label={`${t('examInProgress.questionLabel', { number: i + 1 })}${isAnswered ? ` ${t('examInProgress.questionAnswered')}` : ''}${isFlagged ? ` ${t('examInProgress.questionMarked')}` : ''}`}
825
1271
  >
@@ -885,13 +1331,13 @@ export default function TentativaPage() {
885
1331
  <p className="mb-3 text-sm font-medium">
886
1332
  {t('examInProgress.navigationMobile', {
887
1333
  count: answeredCount,
888
- total: examQuestions.length,
1334
+ total: apiQuestions.length,
889
1335
  })}
890
1336
  </p>
891
1337
  <div className="grid grid-cols-8 gap-2">
892
- {examQuestions.map((q, i) => {
893
- const isAnswered = !!answers[q.id];
894
- const isFlagged = flagged.has(q.id);
1338
+ {apiQuestions.map((q, i) => {
1339
+ const isAnswered = hasAnswerValue(answers[String(q.id)]);
1340
+ const isFlagged = flagged.has(String(q.id));
895
1341
  const isCurrent = i === currentIndex;
896
1342
  return (
897
1343
  <button
@@ -932,16 +1378,16 @@ export default function TentativaPage() {
932
1378
  <DialogDescription asChild>
933
1379
  <div>
934
1380
  <p>{t('submitDialog.description')}</p>
935
- <div className="mt-3 flex gap-4 rounded-lg bg-muted p-3 text-sm">
1381
+ <div className="mt-3 grid gap-3 rounded-xl bg-muted p-4 text-sm md:grid-cols-3">
936
1382
  <div>
937
1383
  <p className="font-medium text-primary">
938
- {answeredCount}/{examQuestions.length}
1384
+ {answeredCount}/{apiQuestions.length}
939
1385
  </p>
940
1386
  <p className="text-xs">{t('submitDialog.answeredLabel')}</p>
941
1387
  </div>
942
1388
  <div>
943
1389
  <p className="font-medium text-primary">
944
- {examQuestions.length - answeredCount}
1390
+ {apiQuestions.length - answeredCount}
945
1391
  </p>
946
1392
  <p className="text-xs">{t('submitDialog.blankLabel')}</p>
947
1393
  </div>
@@ -950,10 +1396,10 @@ export default function TentativaPage() {
950
1396
  <p className="text-xs">{t('submitDialog.markedLabel')}</p>
951
1397
  </div>
952
1398
  </div>
953
- {examQuestions.length - answeredCount > 0 && (
1399
+ {apiQuestions.length - answeredCount > 0 && (
954
1400
  <p className="mt-2 text-sm text-destructive">
955
1401
  {t('submitDialog.warning', {
956
- count: examQuestions.length - answeredCount,
1402
+ count: apiQuestions.length - answeredCount,
957
1403
  })}
958
1404
  </p>
959
1405
  )}
@@ -967,7 +1413,7 @@ export default function TentativaPage() {
967
1413
  >
968
1414
  {t('submitDialog.continueButton')}
969
1415
  </Button>
970
- <Button onClick={handleSubmit}>
1416
+ <Button onClick={() => handleSubmit()}>
971
1417
  {t('submitDialog.submitButton')}
972
1418
  </Button>
973
1419
  </DialogFooter>