@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,6 +5,8 @@ import {
5
5
  Page,
6
6
  PageHeader,
7
7
  PaginationFooter,
8
+ SearchBar,
9
+ ViewModeToggle,
8
10
  } from '@/components/entity-list';
9
11
  import { Badge } from '@/components/ui/badge';
10
12
  import { Button } from '@/components/ui/button';
@@ -31,6 +33,7 @@ import {
31
33
  FieldLabel,
32
34
  } from '@/components/ui/field';
33
35
  import { Input } from '@/components/ui/input';
36
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
34
37
  import {
35
38
  Select,
36
39
  SelectContent,
@@ -48,50 +51,42 @@ import {
48
51
  } from '@/components/ui/sheet';
49
52
  import { Skeleton } from '@/components/ui/skeleton';
50
53
  import { Switch } from '@/components/ui/switch';
54
+ import {
55
+ Table,
56
+ TableBody,
57
+ TableCell,
58
+ TableHead,
59
+ TableHeader,
60
+ TableRow,
61
+ } from '@/components/ui/table';
62
+ import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
63
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
51
64
  import { zodResolver } from '@hookform/resolvers/zod';
52
65
  import { motion } from 'framer-motion';
53
66
  import {
54
67
  AlertTriangle,
55
- BarChart3,
56
- BookOpen,
57
68
  CheckCircle2,
58
69
  FileCheck,
59
70
  FileQuestion,
60
- GraduationCap,
61
- LayoutDashboard,
62
71
  ListChecks,
63
72
  Loader2,
64
73
  MoreHorizontal,
65
74
  Pencil,
66
- Play,
67
75
  Plus,
68
- Search,
69
76
  Shuffle,
70
77
  Target,
71
78
  Timer,
72
79
  Trash2,
73
80
  TrendingUp,
74
81
  Users,
75
- X,
76
82
  } from 'lucide-react';
77
83
  import { useTranslations } from 'next-intl';
78
- import { usePathname, useRouter } from 'next/navigation';
84
+ import { useRouter } from 'next/navigation';
79
85
  import { useEffect, useMemo, useRef, useState } from 'react';
80
- import { Controller, useForm } from 'react-hook-form';
86
+ import { Controller, useForm, useWatch } from 'react-hook-form';
81
87
  import { toast } from 'sonner';
82
88
  import { z } from 'zod';
83
89
 
84
- // ── Nav ───────────────────────────────────────────────────────────────────────
85
-
86
- const NAV_ITEMS = [
87
- { label: 'Dashboard', href: '/', icon: LayoutDashboard },
88
- { label: 'Cursos', href: '/cursos', icon: BookOpen },
89
- { label: 'Turmas', href: '/turmas', icon: Users },
90
- { label: 'Exames', href: '/exames', icon: FileCheck },
91
- { label: 'Formacoes', href: '/formacoes', icon: GraduationCap },
92
- { label: 'Relatorios', href: '/relatorios', icon: BarChart3 },
93
- ];
94
-
95
90
  // ── Types ─────────────────────────────────────────────────────────────────────
96
91
 
97
92
  interface Exame {
@@ -106,13 +101,56 @@ interface Exame {
106
101
  realizacoes: number;
107
102
  mediaNotas: number;
108
103
  criadoEm: string;
104
+ primaryColor?: string | null;
105
+ secondaryColor?: string | null;
109
106
  }
110
107
 
108
+ type ApiExame = {
109
+ id: number;
110
+ code: string;
111
+ title: string;
112
+ minScore: number;
113
+ timeLimit: number;
114
+ shuffle: boolean;
115
+ status: 'published' | 'draft' | 'closed' | 'archived';
116
+ questions: number;
117
+ attempts: number;
118
+ averageScore: number;
119
+ primaryColor?: string | null;
120
+ primaryContrastColor?: string | null;
121
+ secondaryColor?: string | null;
122
+ secondaryContrastColor?: string | null;
123
+ createdAt: string;
124
+ };
125
+
126
+ type ApiExameList = {
127
+ data: ApiExame[];
128
+ total: number;
129
+ page: number;
130
+ pageSize: number;
131
+ lastPage: number;
132
+ };
133
+
134
+ type ApiExameStats = {
135
+ totalExams: number;
136
+ publishedExams: number;
137
+ totalQuestions: number;
138
+ overallAverage: number;
139
+ };
140
+ type ViewMode = 'cards' | 'list';
141
+
142
+ type Locale = {
143
+ id?: number;
144
+ code: string;
145
+ name: string;
146
+ };
147
+
148
+ const API_EXAMS_CACHE_KEY = 'lms:exams:api-cache';
149
+
111
150
  // ── Schema ────────────────────────────────────────────────────────────────────
112
151
 
113
152
  const createExameSchema = (t: (key: string) => string) =>
114
153
  z.object({
115
- codigo: z.string().min(2, t('form.validation.codeMinLength')),
116
154
  titulo: z.string().min(3, t('form.validation.titleMinLength')),
117
155
  notaMinima: z.coerce
118
156
  .number()
@@ -121,20 +159,57 @@ const createExameSchema = (t: (key: string) => string) =>
121
159
  limiteTempo: z.coerce.number().min(1, t('form.validation.timeLimitMin')),
122
160
  shuffle: z.boolean().default(false),
123
161
  status: z.string().min(1, t('form.validation.statusRequired')),
162
+ primaryColor: z
163
+ .string()
164
+ .regex(/^#([0-9A-Fa-f]{6})$/, 'Cor primaria invalida')
165
+ .default('#1D4ED8'),
166
+ secondaryColor: z
167
+ .string()
168
+ .regex(/^#([0-9A-Fa-f]{6})$/, 'Cor secundaria invalida')
169
+ .default('#111827'),
124
170
  });
125
171
 
126
172
  type ExameForm = z.infer<ReturnType<typeof createExameSchema>>;
127
173
 
128
- // ── Constants ─────────────────────────────────────────────────────────────────
174
+ function toApiStatus(status: string) {
175
+ if (status === 'publicado') return 'published';
176
+ if (status === 'rascunho') return 'draft';
177
+ if (status === 'encerrado') return 'closed';
178
+ return status;
179
+ }
129
180
 
130
- const STATUS_MAP: Record<
131
- string,
132
- { label: string; variant: 'default' | 'secondary' | 'outline' }
133
- > = {
134
- publicado: { label: 'Publicado', variant: 'default' },
135
- rascunho: { label: 'Rascunho', variant: 'secondary' },
136
- encerrado: { label: 'Encerrado', variant: 'outline' },
137
- };
181
+ function toPtStatus(status: ApiExame['status']): Exame['status'] {
182
+ if (status === 'published') return 'publicado';
183
+ if (status === 'draft') return 'rascunho';
184
+ return 'encerrado';
185
+ }
186
+
187
+ function mapApiExame(exame: ApiExame): Exame {
188
+ return {
189
+ id: exame.id,
190
+ codigo: exame.code,
191
+ titulo: exame.title,
192
+ notaMinima: exame.minScore,
193
+ limiteTempo: exame.timeLimit,
194
+ shuffle: exame.shuffle,
195
+ status: toPtStatus(exame.status),
196
+ questoes: exame.questions,
197
+ realizacoes: exame.attempts,
198
+ mediaNotas: exame.averageScore,
199
+ criadoEm: exame.createdAt,
200
+ primaryColor: exame.primaryColor ?? null,
201
+ secondaryColor: exame.secondaryColor ?? null,
202
+ };
203
+ }
204
+
205
+ function toNumberOrFallback(value: unknown, fallback: number) {
206
+ if (typeof value === 'string' && value.trim() === '') return fallback;
207
+
208
+ const parsed = Number(value);
209
+ return Number.isFinite(parsed) ? parsed : fallback;
210
+ }
211
+
212
+ // ── Constants ─────────────────────────────────────────────────────────────────
138
213
 
139
214
  const PAGE_SIZES = [6, 12, 24];
140
215
 
@@ -145,141 +220,6 @@ function formatTempo(minutos: number) {
145
220
  return m > 0 ? `${h}h ${m}min` : `${h}h`;
146
221
  }
147
222
 
148
- // ── Seed Data ─────────────────────────────────────────────────────────────────
149
-
150
- const initialExames: Exame[] = [
151
- {
152
- id: 1,
153
- codigo: 'EX-001',
154
- titulo: 'Prova Final - React Avancado',
155
- notaMinima: 7,
156
- limiteTempo: 120,
157
- shuffle: true,
158
- status: 'publicado',
159
- questoes: 40,
160
- realizacoes: 218,
161
- mediaNotas: 7.8,
162
- criadoEm: '2024-04-01',
163
- },
164
- {
165
- id: 2,
166
- codigo: 'EX-002',
167
- titulo: 'Quiz - Fundamentos de UX',
168
- notaMinima: 6,
169
- limiteTempo: 30,
170
- shuffle: true,
171
- status: 'publicado',
172
- questoes: 15,
173
- realizacoes: 156,
174
- mediaNotas: 8.2,
175
- criadoEm: '2024-04-05',
176
- },
177
- {
178
- id: 3,
179
- codigo: 'EX-003',
180
- titulo: 'Simulado - Scrum Master',
181
- notaMinima: 7.5,
182
- limiteTempo: 90,
183
- shuffle: true,
184
- status: 'publicado',
185
- questoes: 60,
186
- realizacoes: 287,
187
- mediaNotas: 6.9,
188
- criadoEm: '2024-03-20',
189
- },
190
- {
191
- id: 4,
192
- codigo: 'EX-004',
193
- titulo: 'Trabalho Pratico - Python',
194
- notaMinima: 7,
195
- limiteTempo: 480,
196
- shuffle: false,
197
- status: 'publicado',
198
- questoes: 5,
199
- realizacoes: 145,
200
- mediaNotas: 7.5,
201
- criadoEm: '2024-05-01',
202
- },
203
- {
204
- id: 5,
205
- codigo: 'EX-005',
206
- titulo: 'Prova Intermediaria - Node.js',
207
- notaMinima: 6,
208
- limiteTempo: 90,
209
- shuffle: true,
210
- status: 'publicado',
211
- questoes: 30,
212
- realizacoes: 134,
213
- mediaNotas: 7.1,
214
- criadoEm: '2024-04-15',
215
- },
216
- {
217
- id: 6,
218
- codigo: 'EX-006',
219
- titulo: 'Quiz - Marketing de Conteudo',
220
- notaMinima: 5,
221
- limiteTempo: 20,
222
- shuffle: true,
223
- status: 'rascunho',
224
- questoes: 10,
225
- realizacoes: 0,
226
- mediaNotas: 0,
227
- criadoEm: '2024-05-10',
228
- },
229
- {
230
- id: 7,
231
- codigo: 'EX-007',
232
- titulo: 'Prova Final - TypeScript',
233
- notaMinima: 7,
234
- limiteTempo: 120,
235
- shuffle: true,
236
- status: 'publicado',
237
- questoes: 45,
238
- realizacoes: 178,
239
- mediaNotas: 7.3,
240
- criadoEm: '2024-06-01',
241
- },
242
- {
243
- id: 8,
244
- codigo: 'EX-008',
245
- titulo: 'Trabalho - Design System',
246
- notaMinima: 8,
247
- limiteTempo: 960,
248
- shuffle: false,
249
- status: 'encerrado',
250
- questoes: 3,
251
- realizacoes: 82,
252
- mediaNotas: 8.5,
253
- criadoEm: '2024-02-15',
254
- },
255
- {
256
- id: 9,
257
- codigo: 'EX-009',
258
- titulo: 'Simulado - Excel Avancado',
259
- notaMinima: 6,
260
- limiteTempo: 60,
261
- shuffle: true,
262
- status: 'encerrado',
263
- questoes: 25,
264
- realizacoes: 498,
265
- mediaNotas: 7.7,
266
- criadoEm: '2024-01-20',
267
- },
268
- {
269
- id: 10,
270
- codigo: 'EX-010',
271
- titulo: 'Quiz - Lideranca Situacional',
272
- notaMinima: 6,
273
- limiteTempo: 25,
274
- shuffle: true,
275
- status: 'publicado',
276
- questoes: 12,
277
- realizacoes: 67,
278
- mediaNotas: 8.0,
279
- criadoEm: '2024-05-20',
280
- },
281
- ];
282
-
283
223
  // ── Animations ────────────────────────────────────────────────────────────────
284
224
 
285
225
  const fadeUp = {
@@ -295,27 +235,29 @@ const stagger = {
295
235
 
296
236
  export default function ExamesPage() {
297
237
  const t = useTranslations('lms.ExamsPage');
298
- const pathname = usePathname();
299
238
  const router = useRouter();
239
+ const { request } = useApp();
300
240
 
301
241
  const exameSchema = useMemo(() => createExameSchema(t), [t]);
302
242
 
303
- const [loading, setLoading] = useState(true);
304
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
305
- const [exames, setExames] = useState<Exame[]>(initialExames);
306
243
  const [sheetOpen, setSheetOpen] = useState(false);
307
244
  const [editingExame, setEditingExame] = useState<Exame | null>(null);
308
245
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
309
246
  const [exameToDelete, setExameToDelete] = useState<Exame | null>(null);
310
247
  const [saving, setSaving] = useState(false);
248
+ const [cachedExameList, setCachedExameList] = useState<ApiExameList | null>(
249
+ null
250
+ );
311
251
 
312
252
  // Search/filter inputs
313
253
  const [buscaInput, setBuscaInput] = useState('');
314
254
  const [filtroStatusInput, setFiltroStatusInput] = useState('todos');
315
-
316
- // Applied filters
317
- const [buscaApplied, setBuscaApplied] = useState('');
318
- const [filtroStatusApplied, setFiltroStatusApplied] = useState('todos');
255
+ const [debouncedBuscaInput, setDebouncedBuscaInput] = useState('');
256
+ const [viewMode, setViewMode] = usePersistedViewMode<ViewMode>({
257
+ storageKey: 'lms:exams:view-mode',
258
+ defaultValue: 'cards',
259
+ allowedValues: ['cards', 'list'],
260
+ });
319
261
 
320
262
  // Pagination
321
263
  const [currentPage, setCurrentPage] = useState(1);
@@ -329,59 +271,150 @@ export default function ExamesPage() {
329
271
  const form = useForm<ExameForm>({
330
272
  resolver: zodResolver(exameSchema),
331
273
  defaultValues: {
332
- codigo: '',
333
274
  titulo: '',
334
275
  notaMinima: 7,
335
276
  limiteTempo: 60,
336
277
  shuffle: true,
337
278
  status: 'rascunho',
279
+ primaryColor: '#1D4ED8',
280
+ secondaryColor: '#111827',
281
+ },
282
+ });
283
+ const watchedFormValues = useWatch({ control: form.control });
284
+
285
+ const {
286
+ data: exameList,
287
+ isLoading,
288
+ isFetching,
289
+ refetch: refetchExams,
290
+ } = useQuery<ApiExameList>({
291
+ queryKey: [
292
+ 'lms-exams',
293
+ currentPage,
294
+ pageSize,
295
+ debouncedBuscaInput,
296
+ filtroStatusInput,
297
+ ],
298
+ queryFn: async () => {
299
+ const response = await request<ApiExameList>({
300
+ url: '/lms/exams',
301
+ method: 'GET',
302
+ params: {
303
+ page: currentPage,
304
+ pageSize,
305
+ ...(debouncedBuscaInput ? { search: debouncedBuscaInput } : {}),
306
+ ...(filtroStatusInput !== 'todos'
307
+ ? { status: toApiStatus(filtroStatusInput) }
308
+ : {}),
309
+ },
310
+ });
311
+ return response.data;
312
+ },
313
+ });
314
+
315
+ const { data: statsData, refetch: refetchStats } = useQuery<ApiExameStats>({
316
+ queryKey: ['lms-exams-stats'],
317
+ queryFn: async () => {
318
+ const response = await request<ApiExameStats>({
319
+ url: '/lms/exams/stats',
320
+ method: 'GET',
321
+ });
322
+ return response.data;
338
323
  },
339
324
  });
340
325
 
341
326
  useEffect(() => {
342
- const t = setTimeout(() => setLoading(false), 700);
343
- return () => clearTimeout(t);
327
+ if (typeof window === 'undefined') return;
328
+ try {
329
+ const raw = window.localStorage.getItem(API_EXAMS_CACHE_KEY);
330
+ if (!raw) return;
331
+ const parsed = JSON.parse(raw) as ApiExameList;
332
+ if (parsed && Array.isArray(parsed.data)) setCachedExameList(parsed);
333
+ } catch {
334
+ setCachedExameList(null);
335
+ }
344
336
  }, []);
345
337
 
346
- // ── Filtering ────────────────────────────────────────────────────────────
347
-
348
- const filteredExames = useMemo(
349
- () =>
350
- exames.filter((e) => {
351
- const q = buscaApplied.toLowerCase();
352
- return (
353
- (!q ||
354
- e.titulo.toLowerCase().includes(q) ||
355
- e.codigo.toLowerCase().includes(q)) &&
356
- (filtroStatusApplied === 'todos' || e.status === filtroStatusApplied)
357
- );
358
- }),
359
- [exames, buscaApplied, filtroStatusApplied]
338
+ useEffect(() => {
339
+ if (typeof window === 'undefined') return;
340
+ if (!exameList) return;
341
+ window.localStorage.setItem(API_EXAMS_CACHE_KEY, JSON.stringify(exameList));
342
+ setCachedExameList(exameList);
343
+ }, [exameList]);
344
+
345
+ const effectiveExameList = exameList ?? cachedExameList;
346
+ const initialLoading = isLoading && !effectiveExameList;
347
+ const cardsRefreshing = isFetching && !!effectiveExameList;
348
+
349
+ const exames = useMemo(
350
+ () => (effectiveExameList?.data ?? []).map((item) => mapApiExame(item)),
351
+ [effectiveExameList]
360
352
  );
353
+ const previewExame = useMemo(() => {
354
+ if (!sheetOpen || !editingExame) return null;
361
355
 
362
- const totalPages = Math.max(1, Math.ceil(filteredExames.length / pageSize));
356
+ return {
357
+ ...editingExame,
358
+ titulo: watchedFormValues.titulo ?? editingExame.titulo,
359
+ notaMinima: toNumberOrFallback(
360
+ watchedFormValues.notaMinima,
361
+ editingExame.notaMinima
362
+ ),
363
+ limiteTempo: toNumberOrFallback(
364
+ watchedFormValues.limiteTempo,
365
+ editingExame.limiteTempo
366
+ ),
367
+ shuffle: watchedFormValues.shuffle ?? editingExame.shuffle,
368
+ status:
369
+ (watchedFormValues.status as Exame['status'] | undefined) ??
370
+ editingExame.status,
371
+ primaryColor: watchedFormValues.primaryColor ?? editingExame.primaryColor,
372
+ secondaryColor:
373
+ watchedFormValues.secondaryColor ?? editingExame.secondaryColor,
374
+ } satisfies Exame;
375
+ }, [editingExame, sheetOpen, watchedFormValues]);
376
+ const totalPages = Math.max(1, effectiveExameList?.lastPage ?? 1);
363
377
  const safePage = Math.min(currentPage, totalPages);
364
- const paginatedExames = filteredExames.slice(
365
- (safePage - 1) * pageSize,
366
- safePage * pageSize
367
- );
378
+ const paginatedExames = useMemo(() => {
379
+ if (!previewExame) return exames;
380
+
381
+ return exames.map((exame) =>
382
+ exame.id === previewExame.id ? previewExame : exame
383
+ );
384
+ }, [exames, previewExame]);
385
+
386
+ useEffect(() => {
387
+ if (currentPage > totalPages) {
388
+ setCurrentPage(totalPages);
389
+ }
390
+ }, [currentPage, totalPages]);
368
391
 
369
- function handleSearch(e: React.FormEvent) {
370
- e.preventDefault();
371
- setBuscaApplied(buscaInput);
372
- setFiltroStatusApplied(filtroStatusInput);
392
+ useEffect(() => {
393
+ const timer = setTimeout(() => {
394
+ setDebouncedBuscaInput(buscaInput.trim());
395
+ }, 350);
396
+
397
+ return () => clearTimeout(timer);
398
+ }, [buscaInput]);
399
+
400
+ useEffect(() => {
373
401
  setCurrentPage(1);
374
- }
402
+ }, [debouncedBuscaInput, filtroStatusInput]);
375
403
 
376
404
  function clearFilters() {
377
405
  setBuscaInput('');
378
406
  setFiltroStatusInput('todos');
379
- setBuscaApplied('');
380
- setFiltroStatusApplied('todos');
407
+ setDebouncedBuscaInput('');
381
408
  setCurrentPage(1);
382
409
  }
383
410
 
384
- const hasActiveFilters = buscaApplied || filtroStatusApplied !== 'todos';
411
+ const hasActiveFilters = buscaInput || filtroStatusInput !== 'todos';
412
+
413
+ function openDeleteDialog(exame: Exame, e: React.MouseEvent) {
414
+ e.stopPropagation();
415
+ setExameToDelete(exame);
416
+ setDeleteDialogOpen(true);
417
+ }
385
418
 
386
419
  // ── Double-click ──────────────────────────────────────────────────────────
387
420
 
@@ -402,12 +435,13 @@ export default function ExamesPage() {
402
435
  function openCreateSheet() {
403
436
  setEditingExame(null);
404
437
  form.reset({
405
- codigo: '',
406
438
  titulo: '',
407
439
  notaMinima: 7,
408
440
  limiteTempo: 60,
409
441
  shuffle: true,
410
442
  status: 'rascunho',
443
+ primaryColor: '#1D4ED8',
444
+ secondaryColor: '#111827',
411
445
  });
412
446
  setSheetOpen(true);
413
447
  }
@@ -416,55 +450,71 @@ export default function ExamesPage() {
416
450
  e?.stopPropagation();
417
451
  setEditingExame(exame);
418
452
  form.reset({
419
- codigo: exame.codigo,
420
453
  titulo: exame.titulo,
421
454
  notaMinima: exame.notaMinima,
422
455
  limiteTempo: exame.limiteTempo,
423
456
  shuffle: exame.shuffle,
424
457
  status: exame.status,
458
+ primaryColor: exame.primaryColor ?? '#1D4ED8',
459
+ secondaryColor: exame.secondaryColor ?? '#111827',
425
460
  });
426
461
  setSheetOpen(true);
427
462
  }
428
463
 
429
464
  async function onSubmit(data: ExameForm) {
430
465
  setSaving(true);
431
- await new Promise((r) => setTimeout(r, 500));
432
- if (editingExame) {
433
- setExames((prev) =>
434
- prev.map((e) =>
435
- e.id === editingExame.id
436
- ? { ...e, ...data, status: data.status as Exame['status'] }
437
- : e
438
- )
439
- );
440
- toast.success(t('toasts.examUpdated'));
441
- } else {
442
- const newExame: Exame = {
443
- id: Date.now(),
444
- ...data,
445
- status: data.status as Exame['status'],
446
- questoes: 0,
447
- realizacoes: 0,
448
- mediaNotas: 0,
449
- criadoEm: new Date().toISOString().split('T')[0] || '',
466
+ try {
467
+ const payload = {
468
+ title: data.titulo,
469
+ minScore: data.notaMinima,
470
+ timeLimit: data.limiteTempo,
471
+ shuffle: data.shuffle,
472
+ status: toApiStatus(data.status),
473
+ primaryColor: data.primaryColor,
474
+ secondaryColor: data.secondaryColor,
450
475
  };
451
- setExames((prev) => [newExame, ...prev]);
452
- toast.success(t('toasts.examCreated'));
453
- setSaving(false);
476
+
477
+ if (editingExame) {
478
+ await request({
479
+ url: `/lms/exams/${editingExame.id}`,
480
+ method: 'PATCH',
481
+ data: payload,
482
+ });
483
+ toast.success(t('toasts.examUpdated'));
484
+ } else {
485
+ const response = await request<ApiExame>({
486
+ url: '/lms/exams',
487
+ method: 'POST',
488
+ data: payload,
489
+ });
490
+ toast.success(t('toasts.examCreated'));
491
+ if (response.data?.id) {
492
+ setTimeout(
493
+ () => router.push(`/lms/exams/${response.data.id}/questions`),
494
+ 400
495
+ );
496
+ }
497
+ }
498
+
499
+ await Promise.all([refetchExams(), refetchStats()]);
454
500
  setSheetOpen(false);
455
- setTimeout(() => router.push(`/lms/exams/${newExame.id}/questions`), 400);
456
- return;
501
+ } finally {
502
+ setSaving(false);
457
503
  }
458
- setSaving(false);
459
- setSheetOpen(false);
460
504
  }
461
505
 
462
- function confirmDelete() {
506
+ async function confirmDelete() {
463
507
  if (!exameToDelete) return;
464
- setExames((prev) => prev.filter((e) => e.id !== exameToDelete.id));
508
+
509
+ await request({
510
+ url: `/lms/exams/${exameToDelete.id}`,
511
+ method: 'DELETE',
512
+ });
513
+
465
514
  toast.success(t('toasts.examRemoved'));
466
515
  setExameToDelete(null);
467
516
  setDeleteDialogOpen(false);
517
+ await Promise.all([refetchExams(), refetchStats()]);
468
518
  }
469
519
 
470
520
  // ── KPIs ──────────────────────────────────────────────────────────────────
@@ -478,48 +528,62 @@ export default function ExamesPage() {
478
528
  encerrado: { label: t('status.closed'), variant: 'outline' },
479
529
  };
480
530
 
481
- const totalQuestoes = exames.reduce((a, e) => a + e.questoes, 0);
482
- const mediaGeral =
483
- exames
484
- .filter((e) => e.mediaNotas > 0)
485
- .reduce((a, e) => a + e.mediaNotas, 0) /
486
- Math.max(exames.filter((e) => e.mediaNotas > 0).length, 1);
487
-
488
531
  const kpis = [
489
532
  {
490
- label: t('kpis.totalExams.label'),
491
- valor: exames.length,
492
- sub: t('kpis.totalExams.sub'),
533
+ key: 'total-exams',
534
+ title: t('kpis.totalExams.label'),
535
+ value: statsData?.totalExams ?? effectiveExameList?.total ?? 0,
536
+ description: t('kpis.totalExams.sub'),
493
537
  icon: FileCheck,
494
- iconBg: 'bg-orange-100',
495
- iconColor: 'text-orange-600',
538
+ layout: 'compact' as const,
539
+ accentClassName: 'from-orange-500/25 via-amber-500/15 to-transparent',
540
+ iconContainerClassName: 'bg-orange-100 text-orange-700',
496
541
  },
497
542
  {
498
- label: t('kpis.published.label'),
499
- valor: exames.filter((e) => e.status === 'publicado').length,
500
- sub: t('kpis.published.sub'),
543
+ key: 'published-exams',
544
+ title: t('kpis.published.label'),
545
+ value: statsData?.publishedExams ?? 0,
546
+ description: t('kpis.published.sub'),
501
547
  icon: CheckCircle2,
502
- iconBg: 'bg-muted',
503
- iconColor: 'text-foreground',
548
+ layout: 'compact' as const,
549
+ accentClassName: 'from-sky-500/25 via-blue-500/15 to-transparent',
550
+ iconContainerClassName: 'bg-sky-100 text-sky-700',
504
551
  },
505
552
  {
506
- label: t('kpis.totalQuestions.label'),
507
- valor: totalQuestoes.toLocaleString('pt-BR'),
508
- sub: t('kpis.totalQuestions.sub'),
553
+ key: 'total-questions',
554
+ title: t('kpis.totalQuestions.label'),
555
+ value: (statsData?.totalQuestions ?? 0).toLocaleString('pt-BR'),
556
+ description: t('kpis.totalQuestions.sub'),
509
557
  icon: FileQuestion,
510
- iconBg: 'bg-muted',
511
- iconColor: 'text-foreground',
558
+ layout: 'compact' as const,
559
+ accentClassName: 'from-emerald-500/25 via-green-500/15 to-transparent',
560
+ iconContainerClassName: 'bg-emerald-100 text-emerald-700',
512
561
  },
513
562
  {
514
- label: t('kpis.avgScore.label'),
515
- valor: mediaGeral.toFixed(1),
516
- sub: t('kpis.avgScore.sub'),
563
+ key: 'avg-score',
564
+ title: t('kpis.avgScore.label'),
565
+ value: Number(statsData?.overallAverage ?? 0).toFixed(1),
566
+ description: t('kpis.avgScore.sub'),
517
567
  icon: TrendingUp,
518
- iconBg: 'bg-muted',
519
- iconColor: 'text-foreground',
568
+ layout: 'compact' as const,
569
+ accentClassName: 'from-pink-500/25 via-rose-500/15 to-transparent',
570
+ iconContainerClassName: 'bg-pink-100 text-pink-700',
520
571
  },
521
572
  ];
522
573
 
574
+ const { locales } = useApp();
575
+
576
+ const handleNewExam = (): void => {
577
+ const nextLocaleData: Record<string, { name: string }> = {};
578
+ locales.forEach((locale: Locale) => {
579
+ nextLocaleData[locale.code] = {
580
+ name: '',
581
+ };
582
+ });
583
+ void nextLocaleData;
584
+ openCreateSheet();
585
+ };
586
+
523
587
  // ── Render ────────────────────────────────────────────────────────────────
524
588
 
525
589
  return (
@@ -536,329 +600,464 @@ export default function ExamesPage() {
536
600
  label: t('breadcrumbs.exams'),
537
601
  },
538
602
  ]}
539
- actions={
540
- <Button onClick={openCreateSheet} className="shrink-0 gap-2">
541
- <Plus className="size-4" /> {t('actions.createExam')}
542
- </Button>
543
- }
603
+ actions={[
604
+ {
605
+ label: t('actions.createExam'),
606
+ onClick: () => handleNewExam(),
607
+ variant: 'default',
608
+ },
609
+ ]}
544
610
  />
545
611
 
546
612
  {/* KPIs */}
547
- <div className="mb-6 grid grid-cols-2 gap-4 lg:grid-cols-4">
548
- {loading
549
- ? Array.from({ length: 4 }).map((_, i) => (
613
+ <div className="mb-1">
614
+ {initialLoading && !statsData ? (
615
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
616
+ {Array.from({ length: 4 }).map((_, i) => (
550
617
  <Card key={i}>
551
618
  <CardContent className="p-4">
552
619
  <Skeleton className="mb-2 h-8 w-16" />
553
620
  <Skeleton className="h-4 w-28" />
554
621
  </CardContent>
555
622
  </Card>
556
- ))
557
- : kpis.map((kpi, i) => (
558
- <motion.div
559
- key={kpi.label}
560
- initial={{ opacity: 0, y: 12 }}
561
- animate={{ opacity: 1, y: 0 }}
562
- transition={{ delay: i * 0.07 }}
563
- >
564
- <Card className="overflow-hidden">
565
- <CardContent className="flex items-start justify-between p-5">
566
- <div>
567
- <p className="text-sm text-muted-foreground">
568
- {kpi.label}
569
- </p>
570
- <p className="mt-1 text-3xl font-bold tracking-tight">
571
- {kpi.valor}
572
- </p>
573
- <p className="mt-0.5 text-xs text-muted-foreground">
574
- {kpi.sub}
575
- </p>
576
- </div>
577
- <div
578
- className={`flex size-10 shrink-0 items-center justify-center rounded-lg ${kpi.iconBg}`}
579
- >
580
- <kpi.icon className={`size-5 ${kpi.iconColor}`} />
581
- </div>
582
- </CardContent>
583
- </Card>
584
- </motion.div>
585
623
  ))}
624
+ </div>
625
+ ) : (
626
+ <KpiCardsGrid items={kpis} />
627
+ )}
586
628
  </div>
587
629
 
588
630
  {/* Search bar */}
589
- <form onSubmit={handleSearch} className="mb-6 mt-0">
590
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
591
- <div className="relative flex-1">
592
- <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
593
- <Input
594
- placeholder={t('filters.searchPlaceholder')}
595
- value={buscaInput}
596
- onChange={(e) => setBuscaInput(e.target.value)}
597
- className="pl-9"
631
+ <div className="mb-1 space-y-3">
632
+ <SearchBar
633
+ searchQuery={buscaInput}
634
+ onSearchChange={setBuscaInput}
635
+ onSearch={() => {
636
+ setDebouncedBuscaInput(buscaInput.trim());
637
+ setCurrentPage(1);
638
+ }}
639
+ placeholder={t('filters.searchPlaceholder')}
640
+ controls={[
641
+ {
642
+ id: 'status',
643
+ type: 'select',
644
+ value: filtroStatusInput,
645
+ onChange: setFiltroStatusInput,
646
+ placeholder: 'Status',
647
+ options: [
648
+ { value: 'todos', label: t('filters.allStatuses') },
649
+ { value: 'publicado', label: t('status.published') },
650
+ { value: 'rascunho', label: t('status.draft') },
651
+ { value: 'encerrado', label: t('status.closed') },
652
+ ],
653
+ },
654
+ ]}
655
+ afterSearchButton={
656
+ <ViewModeToggle
657
+ viewMode={viewMode}
658
+ onViewModeChange={setViewMode}
659
+ listLabel={t('viewMode.list')}
660
+ cardsLabel={t('viewMode.cards')}
598
661
  />
599
- </div>
600
- <div className="flex flex-wrap items-center gap-2">
601
- <Select
602
- value={filtroStatusInput}
603
- onValueChange={setFiltroStatusInput}
604
- >
605
- <SelectTrigger className="h-9 w-[140px] text-sm">
606
- <SelectValue placeholder="Status" />
607
- </SelectTrigger>
608
- <SelectContent>
609
- <SelectItem value="todos">
610
- {t('filters.allStatuses')}
611
- </SelectItem>
612
- <SelectItem value="publicado">
613
- {t('status.published')}
614
- </SelectItem>
615
- <SelectItem value="rascunho">{t('status.draft')}</SelectItem>
616
- <SelectItem value="encerrado">{t('status.closed')}</SelectItem>
617
- </SelectContent>
618
- </Select>
662
+ }
663
+ />
664
+ <div className="flex flex-wrap items-center justify-between gap-3">
665
+ <p className="text-sm text-muted-foreground">
666
+ {effectiveExameList?.total ?? 0}{' '}
667
+ {(effectiveExameList?.total ?? 0) !== 1
668
+ ? t('pagination.examsPlural')
669
+ : t('pagination.exams')}
670
+ </p>
671
+ <div className="flex items-center gap-2">
619
672
  {hasActiveFilters && (
620
673
  <Button
621
674
  type="button"
622
675
  variant="ghost"
623
676
  size="sm"
624
677
  onClick={clearFilters}
625
- className="h-9 text-muted-foreground"
626
678
  >
627
- <X className="mr-1 size-3.5" /> {t('filters.clear')}
679
+ {t('filters.clear')}
628
680
  </Button>
629
681
  )}
630
- <Button type="submit" size="sm" className="h-9 gap-2">
631
- <Search className="size-3.5" /> {t('filters.search')}
632
- </Button>
682
+ {cardsRefreshing && (
683
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
684
+ )}
633
685
  </div>
634
686
  </div>
635
- </form>
636
-
637
- {/* Cards grid */}
638
- {loading ? (
639
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
640
- {Array.from({ length: 6 }).map((_, i) => (
641
- <Card key={i} className="overflow-hidden">
642
- <CardContent className="p-5">
643
- <Skeleton className="mb-3 h-5 w-20 rounded-full" />
644
- <Skeleton className="mb-1.5 h-5 w-3/4" />
645
- <Skeleton className="mb-4 h-4 w-1/2" />
646
- <div className="flex gap-4">
647
- <Skeleton className="h-4 w-16" />
648
- <Skeleton className="h-4 w-16" />
649
- </div>
650
- </CardContent>
651
- </Card>
652
- ))}
653
- </div>
654
- ) : filteredExames.length === 0 ? (
687
+ </div>
688
+
689
+ {/* Exam list */}
690
+ {initialLoading ? (
691
+ viewMode === 'cards' ? (
692
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
693
+ {Array.from({ length: 6 }).map((_, i) => (
694
+ <Card key={i} className="overflow-hidden">
695
+ <CardContent className="p-5">
696
+ <Skeleton className="mb-3 h-5 w-20 rounded-full" />
697
+ <Skeleton className="mb-1.5 h-5 w-3/4" />
698
+ <Skeleton className="mb-4 h-4 w-1/2" />
699
+ <div className="flex gap-4">
700
+ <Skeleton className="h-4 w-16" />
701
+ <Skeleton className="h-4 w-16" />
702
+ </div>
703
+ </CardContent>
704
+ </Card>
705
+ ))}
706
+ </div>
707
+ ) : (
708
+ <div className="overflow-hidden rounded-xl border border-border/70">
709
+ <Table>
710
+ <TableHeader>
711
+ <TableRow>
712
+ <TableHead>{t('form.fields.title.label')}</TableHead>
713
+ <TableHead>{t('form.fields.status.label')}</TableHead>
714
+ <TableHead>{t('cards.questionsLabel')}</TableHead>
715
+ <TableHead>{t('cards.completionsLabel')}</TableHead>
716
+ <TableHead>{t('cards.durationLabel')}</TableHead>
717
+ <TableHead>{t('cards.minScoreLabel')}</TableHead>
718
+ <TableHead className="w-12" />
719
+ </TableRow>
720
+ </TableHeader>
721
+ <TableBody>
722
+ {Array.from({ length: 6 }).map((_, i) => (
723
+ <TableRow key={i}>
724
+ <TableCell>
725
+ <div className="space-y-1.5">
726
+ <Skeleton className="h-4 w-44" />
727
+ <Skeleton className="h-3 w-20" />
728
+ </div>
729
+ </TableCell>
730
+ <TableCell>
731
+ <Skeleton className="h-5 w-20 rounded-full" />
732
+ </TableCell>
733
+ <TableCell>
734
+ <Skeleton className="h-4 w-12" />
735
+ </TableCell>
736
+ <TableCell>
737
+ <Skeleton className="h-4 w-16" />
738
+ </TableCell>
739
+ <TableCell>
740
+ <Skeleton className="h-4 w-14" />
741
+ </TableCell>
742
+ <TableCell>
743
+ <Skeleton className="h-4 w-10" />
744
+ </TableCell>
745
+ <TableCell>
746
+ <Skeleton className="ml-auto size-8 rounded-md" />
747
+ </TableCell>
748
+ </TableRow>
749
+ ))}
750
+ </TableBody>
751
+ </Table>
752
+ </div>
753
+ )
754
+ ) : (effectiveExameList?.total ?? 0) === 0 ? (
655
755
  <EmptyState
656
- icon={<FileCheck className="h-12 w-12" />}
756
+ icon={<FileCheck className="size-12 text-muted-foreground/40" />}
657
757
  title={t('empty.title')}
658
758
  description={t('empty.description')}
659
759
  actionLabel={t('empty.action')}
760
+ actionIcon={<Plus className="mr-2 size-4" />}
660
761
  onAction={openCreateSheet}
661
- actionIcon={<Plus className="mr-2 h-4 w-4" />}
762
+ className="py-20"
662
763
  />
663
764
  ) : (
664
- <motion.div
665
- className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
666
- variants={stagger}
667
- initial="hidden"
668
- animate="show"
669
- >
670
- {paginatedExames.map((exame) => {
671
- const notaColor =
672
- exame.mediaNotas >= exame.notaMinima
673
- ? 'text-emerald-600'
674
- : exame.mediaNotas > 0
675
- ? 'text-red-600'
676
- : 'text-muted-foreground';
677
-
678
- return (
679
- <motion.div key={exame.id} variants={fadeUp}>
680
- <Card
681
- className="group relative cursor-pointer overflow-hidden border-0 shadow-sm transition-all duration-300 hover:shadow-lg hover:-translate-y-1"
682
- onClick={() => handleCardClick(exame)}
683
- title={t('cards.tooltip')}
684
- >
685
- {/* Top accent */}
686
- <div className="h-1 w-full bg-foreground" />
687
-
688
- <CardContent className="p-5">
689
- {/* Header with Icon + Title + Actions */}
690
- <div className="mb-4 flex items-start gap-3">
691
- {/* Exam icon */}
692
- <div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-muted border">
693
- <FileCheck className="size-6 text-foreground" />
694
- </div>
695
- <div className="min-w-0 flex-1">
696
- <div className="mb-1 flex items-start justify-between gap-2">
697
- <h3 className="line-clamp-2 font-semibold leading-snug text-foreground">
698
- {exame.titulo}
699
- </h3>
700
- <DropdownMenu>
701
- <DropdownMenuTrigger asChild>
702
- <Button
703
- variant="ghost"
704
- size="icon"
705
- className="size-8 shrink-0 -mr-2 -mt-1"
706
- onClick={(e) => e.stopPropagation()}
707
- aria-label={t('cards.actions.label')}
708
- >
709
- <MoreHorizontal className="size-4" />
710
- </Button>
711
- </DropdownMenuTrigger>
712
- <DropdownMenuContent align="end" className="w-52">
713
- <DropdownMenuItem
714
- onClick={(e) => {
715
- e.stopPropagation();
716
- router.push(
717
- `/lms/exams/${exame.id}/questions`
718
- );
719
- }}
720
- >
721
- <ListChecks className="mr-2 size-4" />{' '}
722
- {t('cards.actions.manageQuestions')}
723
- </DropdownMenuItem>
724
- <DropdownMenuItem
725
- onClick={(e) => {
726
- e.stopPropagation();
727
- router.push(`/lms/exams/${exame.id}/attempt`);
728
- }}
729
- >
730
- <Play className="mr-2 size-4" />{' '}
731
- {t('cards.actions.testExam')}
732
- </DropdownMenuItem>
733
- <DropdownMenuSeparator />
734
- <DropdownMenuItem
735
- onClick={(e) => openEditSheet(exame, e)}
736
- >
737
- <Pencil className="mr-2 size-4" />{' '}
738
- {t('cards.actions.edit')}
739
- </DropdownMenuItem>
740
- <DropdownMenuSeparator />
741
- <DropdownMenuItem
742
- className="text-destructive focus:text-destructive"
743
- onClick={(e) => {
744
- e.stopPropagation();
745
- setExameToDelete(exame);
746
- setDeleteDialogOpen(true);
747
- }}
748
- >
749
- <Trash2 className="mr-2 size-4" />{' '}
750
- {t('cards.actions.delete')}
751
- </DropdownMenuItem>
752
- </DropdownMenuContent>
753
- </DropdownMenu>
765
+ <div className="relative">
766
+ {cardsRefreshing && (
767
+ <div className="absolute inset-0 z-10 rounded-2xl bg-background/55 backdrop-blur-[1px]" />
768
+ )}
769
+ {viewMode === 'cards' ? (
770
+ <motion.div
771
+ className={`${'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'} ${cardsRefreshing ? 'pointer-events-none' : ''}`}
772
+ variants={stagger}
773
+ initial="hidden"
774
+ animate="show"
775
+ >
776
+ {paginatedExames.map((exame) => {
777
+ return (
778
+ <motion.div key={exame.id} variants={fadeUp}>
779
+ <Card
780
+ className="group relative h-95 cursor-pointer overflow-hidden border-border/70 shadow-sm transition-all duration-200 hover:border-border hover:shadow-md"
781
+ onClick={() => handleCardClick(exame)}
782
+ title={t('cards.tooltip')}
783
+ >
784
+ <div
785
+ className="absolute inset-x-0 top-0 h-1 w-full"
786
+ style={{
787
+ backgroundColor: exame.primaryColor || '#1D4ED8',
788
+ }}
789
+ />
790
+ <CardContent className="p-4">
791
+ <div className="mb-3 flex items-start gap-3">
792
+ <div className="flex size-11 shrink-0 items-center justify-center rounded-xl border bg-muted/60">
793
+ <FileCheck className="size-5 text-foreground" />
794
+ </div>
795
+ <div className="min-w-0 flex-1">
796
+ <div className="mb-1 flex items-start justify-between gap-2">
797
+ <h3 className="line-clamp-2 font-semibold leading-snug text-foreground">
798
+ {exame.titulo}
799
+ </h3>
800
+ <DropdownMenu>
801
+ <DropdownMenuTrigger asChild>
802
+ <Button
803
+ variant="ghost"
804
+ size="icon"
805
+ className="size-8 shrink-0 -mr-2 -mt-1"
806
+ onClick={(e) => e.stopPropagation()}
807
+ aria-label={t('cards.actions.label')}
808
+ >
809
+ <MoreHorizontal className="size-4" />
810
+ </Button>
811
+ </DropdownMenuTrigger>
812
+ <DropdownMenuContent
813
+ align="end"
814
+ className="w-52"
815
+ >
816
+ <DropdownMenuItem
817
+ onClick={(e) => {
818
+ e.stopPropagation();
819
+ router.push(
820
+ `/lms/exams/${exame.id}/questions`
821
+ );
822
+ }}
823
+ >
824
+ <ListChecks className="mr-2 size-4" />{' '}
825
+ {t('cards.actions.manageQuestions')}
826
+ </DropdownMenuItem>
827
+ <DropdownMenuSeparator />
828
+ <DropdownMenuItem
829
+ onClick={(e) => openEditSheet(exame, e)}
830
+ >
831
+ <Pencil className="mr-2 size-4" />{' '}
832
+ {t('cards.actions.edit')}
833
+ </DropdownMenuItem>
834
+ <DropdownMenuSeparator />
835
+ <DropdownMenuItem
836
+ className="text-destructive focus:text-destructive"
837
+ onClick={(e) => openDeleteDialog(exame, e)}
838
+ >
839
+ <Trash2 className="mr-2 size-4" />{' '}
840
+ {t('cards.actions.delete')}
841
+ </DropdownMenuItem>
842
+ </DropdownMenuContent>
843
+ </DropdownMenu>
844
+ </div>
845
+ <p className="text-xs text-muted-foreground">
846
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
847
+ {exame.codigo}
848
+ </code>
849
+ </p>
850
+ </div>
754
851
  </div>
755
- <p className="text-xs text-muted-foreground">
756
- <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
757
- {exame.codigo}
758
- </code>
759
- </p>
760
- </div>
761
- </div>
762
852
 
763
- {/* Badges */}
764
- <div className="mb-4 flex flex-wrap items-center gap-1.5">
765
- <Badge
766
- variant={STATUS_MAP[exame.status]?.variant || 'default'}
767
- className="text-[11px]"
768
- >
769
- {STATUS_MAP[exame.status]?.label}
770
- </Badge>
771
- {exame.shuffle && (
772
- <span className="inline-flex items-center gap-1 rounded-full bg-muted border px-2.5 py-0.5 text-[11px] font-medium text-foreground">
773
- <Shuffle className="size-3" />{' '}
774
- {t('cards.randomLabel')}
775
- </span>
776
- )}
777
- </div>
853
+ <div className="mb-3 flex flex-wrap items-center gap-1.5">
854
+ <Badge
855
+ variant={
856
+ STATUS_MAP[exame.status]?.variant || 'default'
857
+ }
858
+ className="text-[11px]"
859
+ >
860
+ {STATUS_MAP[exame.status]?.label}
861
+ </Badge>
862
+ {exame.shuffle && (
863
+ <span className="inline-flex items-center gap-1 rounded-full border bg-muted px-2.5 py-0.5 text-[11px] font-medium text-foreground">
864
+ <Shuffle className="size-3" />{' '}
865
+ {t('cards.randomLabel')}
866
+ </span>
867
+ )}
868
+ </div>
778
869
 
779
- {/* Stats grid */}
780
- <div className="mb-4 grid grid-cols-2 gap-2">
781
- <div className="rounded-xl border bg-muted/40 p-3 text-center">
782
- <div className="mb-1 flex items-center justify-center gap-1 text-[11px] text-muted-foreground">
783
- <FileQuestion className="size-3" />{' '}
784
- {t('cards.questionsLabel')}
870
+ <div className="mb-3 grid grid-cols-2 gap-2">
871
+ <div className="rounded-xl border bg-muted/40 p-3 text-center">
872
+ <div className="mb-1 flex items-center justify-center gap-1 text-[11px] text-muted-foreground">
873
+ <FileQuestion className="size-3" />{' '}
874
+ {t('cards.questionsLabel')}
875
+ </div>
876
+ <p className="text-2xl font-bold text-foreground">
877
+ {exame.questoes}
878
+ </p>
879
+ </div>
880
+ <div className="rounded-xl border bg-emerald-50 p-3 text-center">
881
+ <div className="mb-1 flex items-center justify-center gap-1 text-[11px] text-muted-foreground">
882
+ <Users className="size-3" />{' '}
883
+ {t('cards.completionsLabel')}
884
+ </div>
885
+ <p className="text-2xl font-bold text-foreground">
886
+ {exame.realizacoes.toLocaleString('pt-BR')}
887
+ </p>
888
+ </div>
785
889
  </div>
786
- <p className="text-2xl font-bold text-foreground">
787
- {exame.questoes}
788
- </p>
789
- </div>
790
- <div className="rounded-xl border bg-muted/40 p-3 text-center">
791
- <div className="mb-1 flex items-center justify-center gap-1 text-[11px] text-muted-foreground">
792
- <Users className="size-3" />{' '}
793
- {t('cards.completionsLabel')}
890
+
891
+ <div className="mb-3 grid grid-cols-2 gap-2">
892
+ <div className="flex items-center gap-2 rounded-lg bg-muted/40 px-3 py-2">
893
+ <Timer className="size-4 text-muted-foreground" />
894
+ <div>
895
+ <p className="text-[11px] text-muted-foreground">
896
+ {t('cards.durationLabel')}
897
+ </p>
898
+ <p className="text-xs font-semibold">
899
+ {formatTempo(exame.limiteTempo)}
900
+ </p>
901
+ </div>
902
+ </div>
903
+ <div className="flex items-center gap-2 rounded-lg bg-muted/40 px-3 py-2">
904
+ <Target className="size-4 text-muted-foreground" />
905
+ <div>
906
+ <p className="text-[11px] text-muted-foreground">
907
+ {t('cards.minScoreLabel')}
908
+ </p>
909
+ <p className="text-xs font-semibold">
910
+ {exame.notaMinima}
911
+ </p>
912
+ </div>
913
+ </div>
794
914
  </div>
795
- <p className="text-2xl font-bold text-foreground">
796
- {exame.realizacoes.toLocaleString('pt-BR')}
797
- </p>
798
- </div>
799
- </div>
800
915
 
801
- {/* Time and score info */}
802
- <div className="mb-4 grid grid-cols-2 gap-2">
803
- <div className="flex items-center gap-2 rounded-lg bg-muted/40 px-3 py-2">
804
- <Timer className="size-4 text-muted-foreground" />
805
- <div>
806
- <p className="text-[11px] text-muted-foreground">
807
- {t('cards.durationLabel')}
916
+ {exame.realizacoes > 0 && (
917
+ <div className="mt-auto flex items-center justify-between rounded-lg border bg-muted/40 px-3 py-2.5">
918
+ <span className="text-xs text-muted-foreground">
919
+ {t('cards.avgScoreLabel')}
920
+ </span>
921
+ <span
922
+ className={`text-lg font-bold ${
923
+ exame.mediaNotas >= exame.notaMinima
924
+ ? 'text-foreground'
925
+ : 'text-muted-foreground'
926
+ }`}
927
+ >
928
+ {exame.mediaNotas.toFixed(1)}
929
+ </span>
930
+ </div>
931
+ )}
932
+ </CardContent>
933
+ </Card>
934
+ </motion.div>
935
+ );
936
+ })}
937
+ </motion.div>
938
+ ) : (
939
+ <div
940
+ className={`overflow-hidden rounded-xl border border-border/70 ${cardsRefreshing ? 'pointer-events-none' : ''}`}
941
+ >
942
+ <Table>
943
+ <TableHeader>
944
+ <TableRow>
945
+ <TableHead>{t('form.fields.title.label')}</TableHead>
946
+ <TableHead>{t('form.fields.status.label')}</TableHead>
947
+ <TableHead>{t('cards.questionsLabel')}</TableHead>
948
+ <TableHead>{t('cards.completionsLabel')}</TableHead>
949
+ <TableHead>{t('cards.durationLabel')}</TableHead>
950
+ <TableHead>{t('cards.minScoreLabel')}</TableHead>
951
+ <TableHead>{t('cards.avgScoreLabel')}</TableHead>
952
+ <TableHead className="w-12" />
953
+ </TableRow>
954
+ </TableHeader>
955
+ <TableBody>
956
+ {paginatedExames.map((exame) => (
957
+ <TableRow
958
+ key={exame.id}
959
+ className="cursor-pointer"
960
+ onClick={() => handleCardClick(exame)}
961
+ title={t('cards.tooltip')}
962
+ >
963
+ <TableCell>
964
+ <div className="min-w-0">
965
+ <p className="truncate font-semibold text-foreground">
966
+ {exame.titulo}
808
967
  </p>
809
- <p className="text-xs font-semibold">
810
- {formatTempo(exame.limiteTempo)}
968
+ <p className="mt-1 text-xs text-muted-foreground">
969
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">
970
+ {exame.codigo}
971
+ </code>
811
972
  </p>
812
973
  </div>
813
- </div>
814
- <div className="flex items-center gap-2 rounded-lg bg-muted/40 px-3 py-2">
815
- <Target className="size-4 text-muted-foreground" />
816
- <div>
817
- <p className="text-[11px] text-muted-foreground">
818
- {t('cards.minScoreLabel')}
819
- </p>
820
- <p className="text-xs font-semibold">
821
- {exame.notaMinima}
822
- </p>
974
+ </TableCell>
975
+ <TableCell>
976
+ <div className="flex flex-wrap items-center gap-1.5">
977
+ <Badge
978
+ variant={
979
+ STATUS_MAP[exame.status]?.variant || 'default'
980
+ }
981
+ className="text-[11px]"
982
+ >
983
+ {STATUS_MAP[exame.status]?.label}
984
+ </Badge>
985
+ {exame.shuffle && (
986
+ <span className="inline-flex items-center gap-1 rounded-full border bg-muted px-2 py-0.5 text-[11px] font-medium text-foreground">
987
+ <Shuffle className="size-3" />
988
+ </span>
989
+ )}
823
990
  </div>
824
- </div>
825
- </div>
826
-
827
- {/* Average score footer */}
828
- {exame.realizacoes > 0 && (
829
- <div className="flex items-center justify-between rounded-lg bg-muted/40 px-3 py-2.5 border">
830
- <span className="text-xs text-muted-foreground">
831
- {t('cards.avgScoreLabel')}
832
- </span>
833
- <span
834
- className={`text-lg font-bold ${
835
- exame.mediaNotas >= exame.notaMinima
836
- ? 'text-foreground'
837
- : 'text-muted-foreground'
838
- }`}
839
- >
840
- {exame.mediaNotas.toFixed(1)}
841
- </span>
842
- </div>
843
- )}
844
- </CardContent>
845
- </Card>
846
- </motion.div>
847
- );
848
- })}
849
- </motion.div>
991
+ </TableCell>
992
+ <TableCell>{exame.questoes}</TableCell>
993
+ <TableCell>
994
+ {exame.realizacoes.toLocaleString('pt-BR')}
995
+ </TableCell>
996
+ <TableCell>{formatTempo(exame.limiteTempo)}</TableCell>
997
+ <TableCell>{exame.notaMinima}</TableCell>
998
+ <TableCell>
999
+ {exame.realizacoes > 0
1000
+ ? exame.mediaNotas.toFixed(1)
1001
+ : '-'}
1002
+ </TableCell>
1003
+ <TableCell onClick={(e) => e.stopPropagation()}>
1004
+ <DropdownMenu>
1005
+ <DropdownMenuTrigger asChild>
1006
+ <Button
1007
+ variant="ghost"
1008
+ size="icon"
1009
+ className="ml-auto size-8"
1010
+ aria-label={t('cards.actions.label')}
1011
+ >
1012
+ <MoreHorizontal className="size-4" />
1013
+ </Button>
1014
+ </DropdownMenuTrigger>
1015
+ <DropdownMenuContent align="end" className="w-52">
1016
+ <DropdownMenuItem
1017
+ onClick={() =>
1018
+ router.push(`/lms/exams/${exame.id}/questions`)
1019
+ }
1020
+ >
1021
+ <ListChecks className="mr-2 size-4" />{' '}
1022
+ {t('cards.actions.manageQuestions')}
1023
+ </DropdownMenuItem>
1024
+ <DropdownMenuSeparator />
1025
+ <DropdownMenuItem
1026
+ onClick={() => openEditSheet(exame)}
1027
+ >
1028
+ <Pencil className="mr-2 size-4" />{' '}
1029
+ {t('cards.actions.edit')}
1030
+ </DropdownMenuItem>
1031
+ <DropdownMenuSeparator />
1032
+ <DropdownMenuItem
1033
+ className="text-destructive focus:text-destructive"
1034
+ onClick={(e) => openDeleteDialog(exame, e)}
1035
+ >
1036
+ <Trash2 className="mr-2 size-4" />{' '}
1037
+ {t('cards.actions.delete')}
1038
+ </DropdownMenuItem>
1039
+ </DropdownMenuContent>
1040
+ </DropdownMenu>
1041
+ </TableCell>
1042
+ </TableRow>
1043
+ ))}
1044
+ </TableBody>
1045
+ </Table>
1046
+ </div>
1047
+ )}
1048
+ </div>
850
1049
  )}
851
1050
 
852
1051
  {/* Pagination footer */}
853
- {!loading && filteredExames.length > 0 && (
1052
+ {!initialLoading && (effectiveExameList?.total ?? 0) > 0 && (
854
1053
  <div className="mt-6">
855
1054
  <PaginationFooter
856
1055
  currentPage={safePage}
857
1056
  pageSize={pageSize}
858
- totalItems={filteredExames.length}
1057
+ totalItems={effectiveExameList?.total ?? 0}
859
1058
  onPageChange={setCurrentPage}
860
- onPageSizeChange={(nextSize) => {
861
- setPageSize(nextSize);
1059
+ onPageSizeChange={(nextPageSize) => {
1060
+ setPageSize(nextPageSize);
862
1061
  setCurrentPage(1);
863
1062
  }}
864
1063
  pageSizeOptions={PAGE_SIZES}
@@ -886,18 +1085,17 @@ export default function ExamesPage() {
886
1085
  onSubmit={form.handleSubmit(onSubmit)}
887
1086
  className="flex flex-1 flex-col px-4 gap-4 py-6"
888
1087
  >
889
- <Field>
890
- <FieldLabel htmlFor="codigo">
891
- {t('form.fields.code.label')}{' '}
892
- <span className="text-destructive">*</span>
893
- </FieldLabel>
894
- <Input
895
- id="codigo"
896
- placeholder={t('form.fields.code.placeholder')}
897
- {...form.register('codigo')}
898
- />
899
- <FieldError>{form.formState.errors.codigo?.message}</FieldError>
900
- </Field>
1088
+ {editingExame && (
1089
+ <Field>
1090
+ <FieldLabel htmlFor="codigo">
1091
+ {t('form.fields.code.label')}
1092
+ </FieldLabel>
1093
+ <Input id="codigo" value={editingExame.codigo} readOnly />
1094
+ <FieldDescription>
1095
+ Codigo gerado automaticamente pelo sistema.
1096
+ </FieldDescription>
1097
+ </Field>
1098
+ )}
901
1099
  <Field>
902
1100
  <FieldLabel htmlFor="titulo">
903
1101
  {t('form.fields.title.label')}{' '}
@@ -979,6 +1177,61 @@ export default function ExamesPage() {
979
1177
  />
980
1178
  <FieldError>{form.formState.errors.status?.message}</FieldError>
981
1179
  </Field>
1180
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
1181
+ <Field>
1182
+ <FieldLabel htmlFor="primaryColor">Cor Primaria</FieldLabel>
1183
+ <Controller
1184
+ name="primaryColor"
1185
+ control={form.control}
1186
+ render={({ field }) => (
1187
+ <div className="flex items-center gap-2">
1188
+ <Input
1189
+ id="primaryColor"
1190
+ type="color"
1191
+ className="h-10 w-16 p-1"
1192
+ value={field.value}
1193
+ onChange={field.onChange}
1194
+ />
1195
+ <Input
1196
+ value={field.value}
1197
+ onChange={field.onChange}
1198
+ placeholder="#1D4ED8"
1199
+ />
1200
+ </div>
1201
+ )}
1202
+ />
1203
+ <FieldError>
1204
+ {form.formState.errors.primaryColor?.message}
1205
+ </FieldError>
1206
+ </Field>
1207
+
1208
+ <Field>
1209
+ <FieldLabel htmlFor="secondaryColor">Cor Secundaria</FieldLabel>
1210
+ <Controller
1211
+ name="secondaryColor"
1212
+ control={form.control}
1213
+ render={({ field }) => (
1214
+ <div className="flex items-center gap-2">
1215
+ <Input
1216
+ id="secondaryColor"
1217
+ type="color"
1218
+ className="h-10 w-16 p-1"
1219
+ value={field.value}
1220
+ onChange={field.onChange}
1221
+ />
1222
+ <Input
1223
+ value={field.value}
1224
+ onChange={field.onChange}
1225
+ placeholder="#111827"
1226
+ />
1227
+ </div>
1228
+ )}
1229
+ />
1230
+ <FieldError>
1231
+ {form.formState.errors.secondaryColor?.message}
1232
+ </FieldError>
1233
+ </Field>
1234
+ </div>
982
1235
  <label className="flex cursor-pointer items-center justify-between rounded-lg border p-3 hover:bg-muted">
983
1236
  <div>
984
1237
  <p className="text-sm font-medium">
@@ -1013,7 +1266,7 @@ export default function ExamesPage() {
1013
1266
 
1014
1267
  {/* Delete Dialog */}
1015
1268
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
1016
- <DialogContent>
1269
+ <DialogContent className="mx-auto max-w-3xl">
1017
1270
  <DialogHeader>
1018
1271
  <DialogTitle className="flex items-center gap-2">
1019
1272
  <AlertTriangle className="size-5 text-destructive" />{' '}