@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,11 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ SearchBar,
7
+ StatsCards,
8
+ } from '@/components/entity-list';
4
9
  import { Badge } from '@/components/ui/badge';
5
10
  import { Button } from '@/components/ui/button';
6
11
  import {
@@ -10,13 +15,6 @@ import {
10
15
  CardHeader,
11
16
  CardTitle,
12
17
  } from '@/components/ui/card';
13
- import {
14
- Select,
15
- SelectContent,
16
- SelectItem,
17
- SelectTrigger,
18
- SelectValue,
19
- } from '@/components/ui/select';
20
18
  import { Skeleton } from '@/components/ui/skeleton';
21
19
  import {
22
20
  Table,
@@ -26,11 +24,20 @@ import {
26
24
  TableHeader,
27
25
  TableRow,
28
26
  } from '@/components/ui/table';
29
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
27
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
30
28
  import { motion } from 'framer-motion';
31
- import { ArrowUpRight, Download, TrendingDown, TrendingUp } from 'lucide-react';
29
+ import type { jsPDF } from 'jspdf';
30
+ import {
31
+ Activity,
32
+ BookOpen,
33
+ Download,
34
+ GraduationCap,
35
+ TrendingDown,
36
+ Trophy,
37
+ Users,
38
+ } from 'lucide-react';
32
39
  import { useTranslations } from 'next-intl';
33
- import { useEffect, useState } from 'react';
40
+ import { useMemo, useState } from 'react';
34
41
  import {
35
42
  Area,
36
43
  AreaChart,
@@ -38,15 +45,8 @@ import {
38
45
  BarChart,
39
46
  CartesianGrid,
40
47
  Cell,
41
- Line,
42
- LineChart,
43
48
  Pie,
44
49
  PieChart,
45
- PolarAngleAxis,
46
- PolarGrid,
47
- PolarRadiusAxis,
48
- Radar,
49
- RadarChart,
50
50
  ResponsiveContainer,
51
51
  Tooltip,
52
52
  XAxis,
@@ -54,856 +54,850 @@ import {
54
54
  } from 'recharts';
55
55
  import { toast } from 'sonner';
56
56
 
57
- // Data for charts
58
- const monthlyEnrollments = [
59
- { mes: 'jan', matriculas: 120, cancelamentos: 15, receita: 48000 },
60
- { mes: 'feb', matriculas: 150, cancelamentos: 12, receita: 60000 },
61
- { mes: 'mar', matriculas: 180, cancelamentos: 18, receita: 72000 },
62
- { mes: 'apr', matriculas: 200, cancelamentos: 10, receita: 80000 },
63
- { mes: 'may', matriculas: 170, cancelamentos: 22, receita: 68000 },
64
- { mes: 'jun', matriculas: 220, cancelamentos: 8, receita: 88000 },
65
- { mes: 'jul', matriculas: 190, cancelamentos: 14, receita: 76000 },
66
- { mes: 'aug', matriculas: 240, cancelamentos: 11, receita: 96000 },
67
- { mes: 'sep', matriculas: 210, cancelamentos: 16, receita: 84000 },
68
- { mes: 'oct', matriculas: 260, cancelamentos: 9, receita: 104000 },
69
- { mes: 'nov', matriculas: 230, cancelamentos: 13, receita: 92000 },
70
- { mes: 'dec', matriculas: 280, cancelamentos: 7, receita: 112000 },
71
- ];
72
-
73
- const categoryPerformance = [
74
- { area: 'technology', media: 7.8, alunos: 1200, conclusao: 82 },
75
- { area: 'design', media: 8.2, alunos: 650, conclusao: 88 },
76
- { area: 'management', media: 7.5, alunos: 480, conclusao: 76 },
77
- { area: 'marketing', media: 7.9, alunos: 320, conclusao: 79 },
78
- ];
79
-
80
- const courseRanking = [
81
- { nome: 'reactAdvanced', alunos: 245, conclusao: 85, nota: 8.1 },
82
- { nome: 'excelBusiness', alunos: 534, conclusao: 92, nota: 7.9 },
83
- { nome: 'agileProjectManagement', alunos: 312, conclusao: 78, nota: 7.5 },
84
- { nome: 'pythonDataScience', alunos: 178, conclusao: 72, nota: 7.8 },
85
- { nome: 'uxDesignFundamentals', alunos: 189, conclusao: 88, nota: 8.3 },
86
- { nome: 'typescriptPractice', alunos: 201, conclusao: 80, nota: 7.6 },
87
- { nome: 'nodeComplete', alunos: 156, conclusao: 75, nota: 7.4 },
88
- { nome: 'designSystem', alunos: 87, conclusao: 90, nota: 8.5 },
89
- ];
90
-
91
- const radarData = [
92
- { subject: 'engagement', A: 85, fullMark: 100 },
93
- { subject: 'completion', A: 78, fullMark: 100 },
94
- { subject: 'satisfaction', A: 92, fullMark: 100 },
95
- { subject: 'recommendation', A: 88, fullMark: 100 },
96
- { subject: 'retention', A: 72, fullMark: 100 },
97
- { subject: 'performance', A: 81, fullMark: 100 },
98
- ];
99
-
100
- const statusDistribution = [
101
- { nome: 'active', valor: 1865, cor: '#22c55e' }, // green-500
102
- { nome: 'completed', valor: 742, cor: '#3b82f6' }, // blue-500
103
- { nome: 'inactive', valor: 240, cor: '#f97316' }, // orange-500
104
- ];
105
-
106
- const weeklyActivity = [
107
- { dia: 'mon', acessos: 420, aulas: 180, exercicios: 95 },
108
- { dia: 'tue', acessos: 380, aulas: 160, exercicios: 88 },
109
- { dia: 'wed', acessos: 450, aulas: 200, exercicios: 110 },
110
- { dia: 'thu', acessos: 410, aulas: 175, exercicios: 92 },
111
- { dia: 'fri', acessos: 350, aulas: 140, exercicios: 75 },
112
- { dia: 'sat', acessos: 280, aulas: 110, exercicios: 60 },
113
- { dia: 'sun', acessos: 220, aulas: 85, exercicios: 45 },
114
- ];
115
-
116
- export default function RelatoriosPage() {
117
- const t = useTranslations('lms.ReportsPage');
118
- const [loading, setLoading] = useState(true);
57
+ type ApiReportsResponse = {
58
+ kpis: {
59
+ totalStudents: { value: number; change: number; positive: boolean };
60
+ enrollments: { value: number; change: number; positive: boolean };
61
+ completionRate: { value: number; change: number; positive: boolean };
62
+ churnRate: { value: number; change: number; positive: boolean };
63
+ };
64
+ monthlyData: Array<{
65
+ mes: string;
66
+ matriculas: number;
67
+ cancelamentos: number;
68
+ receita: number;
69
+ }>;
70
+ courseRanking: Array<{
71
+ id: number;
72
+ nome: string;
73
+ alunos: number;
74
+ conclusao: number;
75
+ nota: number;
76
+ }>;
77
+ categoryPerformance: Array<{
78
+ area: string;
79
+ alunos: number;
80
+ conclusao: number;
81
+ media: number;
82
+ }>;
83
+ studentStatus: Array<{ nome: string; valor: number; cor: string }>;
84
+ weeklyActivity: Array<{
85
+ dia: string;
86
+ acessos: number;
87
+ aulas: number;
88
+ exercicios: number;
89
+ }>;
90
+ performanceEvolution: Array<{ mes: string; nota: number }>;
91
+ };
92
+
93
+ type HighlightCard = {
94
+ title: string;
95
+ description: string;
96
+ value: string;
97
+ helper: string;
98
+ icon: typeof Trophy;
99
+ };
100
+
101
+ type JsPdfWithAutoTable = jsPDF & {
102
+ lastAutoTable?: {
103
+ finalY: number;
104
+ };
105
+ };
106
+
107
+ const tooltipStyle = {
108
+ backgroundColor: 'hsl(var(--card))',
109
+ border: '1px solid hsl(var(--border))',
110
+ borderRadius: '8px',
111
+ fontSize: '12px',
112
+ };
113
+
114
+ export default function ReportsDashboardPage() {
115
+ const t = useTranslations('lms.ReportsDashboardPage');
116
+ const { request } = useApp();
117
+ const [periodoInput, setPeriodoInput] = useState('12m');
119
118
  const [periodo, setPeriodo] = useState('12m');
120
- const [activeTab, setActiveTab] = useState('geral');
121
-
122
- useEffect(() => {
123
- const timer = setTimeout(() => setLoading(false), 1000);
124
- return () => clearTimeout(timer);
125
- }, []);
126
-
127
- const tooltipStyle = {
128
- backgroundColor: 'hsl(var(--card))',
129
- border: '1px solid hsl(var(--border))',
130
- borderRadius: '8px',
131
- fontSize: '12px',
119
+ const [searchInput, setSearchInput] = useState('');
120
+ const [searchQuery, setSearchQuery] = useState('');
121
+ const [isExporting, setIsExporting] = useState(false);
122
+
123
+ const { data: reportsData, isLoading: loading } =
124
+ useQuery<ApiReportsResponse>({
125
+ queryKey: ['lms-reports-dashboard', periodo],
126
+ queryFn: async () => {
127
+ const response = await request<ApiReportsResponse>({
128
+ url: '/lms/reports',
129
+ method: 'GET',
130
+ params: { period: periodo },
131
+ });
132
+ return response.data;
133
+ },
134
+ });
135
+
136
+ const normalizedSearch = searchQuery.trim().toLocaleLowerCase('pt-BR');
137
+ const monthlyData = reportsData?.monthlyData ?? [];
138
+ const rawCourseRanking = reportsData?.courseRanking;
139
+ const courseRanking = useMemo(
140
+ () => [...(rawCourseRanking ?? [])].sort((a, b) => b.alunos - a.alunos),
141
+ [rawCourseRanking]
142
+ );
143
+ const rawCategoryPerformance = reportsData?.categoryPerformance;
144
+ const categoryPerformance = useMemo(
145
+ () => rawCategoryPerformance ?? [],
146
+ [rawCategoryPerformance]
147
+ );
148
+ const rawStudentStatus = reportsData?.studentStatus;
149
+ const studentStatus = useMemo(
150
+ () => rawStudentStatus ?? [],
151
+ [rawStudentStatus]
152
+ );
153
+ const rawWeeklyActivity = reportsData?.weeklyActivity;
154
+ const weeklyActivity = useMemo(
155
+ () => rawWeeklyActivity ?? [],
156
+ [rawWeeklyActivity]
157
+ );
158
+ const rawPerformanceEvolution = reportsData?.performanceEvolution;
159
+ const performanceEvolution = useMemo(
160
+ () => rawPerformanceEvolution ?? [],
161
+ [rawPerformanceEvolution]
162
+ );
163
+
164
+ const filteredCourseRanking = useMemo(() => {
165
+ return courseRanking.filter((item) => {
166
+ return (
167
+ normalizedSearch.length === 0 ||
168
+ item.nome.toLocaleLowerCase('pt-BR').includes(normalizedSearch)
169
+ );
170
+ });
171
+ }, [courseRanking, normalizedSearch]);
172
+
173
+ const filteredCategoryPerformance = useMemo(() => {
174
+ return categoryPerformance.filter((item) => {
175
+ return (
176
+ normalizedSearch.length === 0 ||
177
+ item.area.toLocaleLowerCase('pt-BR').includes(normalizedSearch)
178
+ );
179
+ });
180
+ }, [categoryPerformance, normalizedSearch]);
181
+
182
+ const filteredStudentStatus = useMemo(() => {
183
+ return studentStatus.filter((item) => {
184
+ return (
185
+ normalizedSearch.length === 0 ||
186
+ t(`statusDistribution.${item.nome}`)
187
+ .toLocaleLowerCase('pt-BR')
188
+ .includes(normalizedSearch)
189
+ );
190
+ });
191
+ }, [normalizedSearch, studentStatus, t]);
192
+
193
+ const topCourse = filteredCourseRanking[0];
194
+ const topPath = useMemo(
195
+ () =>
196
+ [...filteredCategoryPerformance].sort(
197
+ (a, b) => b.conclusao - a.conclusao
198
+ )[0],
199
+ [filteredCategoryPerformance]
200
+ );
201
+ const dominantStatus = useMemo(
202
+ () => [...filteredStudentStatus].sort((a, b) => b.valor - a.valor)[0],
203
+ [filteredStudentStatus]
204
+ );
205
+ const mostActiveDay = useMemo(
206
+ () => [...weeklyActivity].sort((a, b) => b.acessos - a.acessos)[0],
207
+ [weeklyActivity]
208
+ );
209
+
210
+ const averageScore = useMemo(() => {
211
+ if (performanceEvolution.length === 0) return 0;
212
+ const total = performanceEvolution.reduce(
213
+ (sum, item) => sum + item.nota,
214
+ 0
215
+ );
216
+ return total / performanceEvolution.length;
217
+ }, [performanceEvolution]);
218
+
219
+ const totalActivity = useMemo(() => {
220
+ return weeklyActivity.reduce(
221
+ (sum, item) => sum + item.acessos + item.aulas + item.exercicios,
222
+ 0
223
+ );
224
+ }, [weeklyActivity]);
225
+
226
+ const formatChange = (change: number) => {
227
+ const prefix = change >= 0 ? '+' : '';
228
+ return `${prefix}${change}%`;
229
+ };
230
+
231
+ const kpiItems = reportsData
232
+ ? [
233
+ {
234
+ title: t('kpis.totalStudents.title'),
235
+ value: reportsData.kpis.totalStudents.value.toLocaleString('pt-BR'),
236
+ change: formatChange(reportsData.kpis.totalStudents.change),
237
+ },
238
+ {
239
+ title: t('kpis.enrollments.title'),
240
+ value: reportsData.kpis.enrollments.value.toLocaleString('pt-BR'),
241
+ change: formatChange(reportsData.kpis.enrollments.change),
242
+ },
243
+ {
244
+ title: t('kpis.completionRate.title'),
245
+ value: `${reportsData.kpis.completionRate.value}%`,
246
+ change: formatChange(reportsData.kpis.completionRate.change),
247
+ },
248
+ {
249
+ title: t('kpis.churnRate.title'),
250
+ value: `${reportsData.kpis.churnRate.value}%`,
251
+ change: formatChange(reportsData.kpis.churnRate.change),
252
+ },
253
+ ]
254
+ : [];
255
+
256
+ const statsCards = kpiItems.map((item, index) => {
257
+ const icons = [Users, BookOpen, GraduationCap, TrendingDown];
258
+ const bgColors = [
259
+ 'bg-blue-100',
260
+ 'bg-emerald-100',
261
+ 'bg-amber-100',
262
+ 'bg-rose-100',
263
+ ];
264
+ const iconColors = [
265
+ 'text-blue-600',
266
+ 'text-emerald-600',
267
+ 'text-amber-600',
268
+ 'text-rose-600',
269
+ ];
270
+ const Icon = icons[index] ?? Users;
271
+
272
+ return {
273
+ title: `${item.title} ${item.change}`,
274
+ value: item.value,
275
+ icon: <Icon className="h-5 w-5" />,
276
+ iconBgColor: bgColors[index],
277
+ iconColor: iconColors[index],
278
+ };
279
+ });
280
+
281
+ const highlightCards: HighlightCard[] = [
282
+ {
283
+ title: t('highlights.topCourse.title'),
284
+ description: t('highlights.topCourse.description'),
285
+ value: topCourse?.nome ?? t('emptyState.title'),
286
+ helper: topCourse
287
+ ? t('highlights.topCourse.helper', {
288
+ total: topCourse.alunos.toLocaleString('pt-BR'),
289
+ })
290
+ : t('emptyState.description'),
291
+ icon: Trophy,
292
+ },
293
+ {
294
+ title: t('highlights.topPath.title'),
295
+ description: t('highlights.topPath.description'),
296
+ value: topPath?.area ?? t('emptyState.title'),
297
+ helper: topPath
298
+ ? t('highlights.topPath.helper', { total: topPath.conclusao })
299
+ : t('emptyState.description'),
300
+ icon: GraduationCap,
301
+ },
302
+ {
303
+ title: t('highlights.studentStatus.title'),
304
+ description: t('highlights.studentStatus.description'),
305
+ value: dominantStatus
306
+ ? t(`statusDistribution.${dominantStatus.nome}`)
307
+ : t('emptyState.title'),
308
+ helper: dominantStatus
309
+ ? t('highlights.studentStatus.helper', { total: dominantStatus.valor })
310
+ : t('emptyState.description'),
311
+ icon: Users,
312
+ },
313
+ {
314
+ title: t('highlights.activity.title'),
315
+ description: t('highlights.activity.description'),
316
+ value: mostActiveDay
317
+ ? t(`weekdays.${mostActiveDay.dia}`)
318
+ : t('emptyState.title'),
319
+ helper: t('highlights.activity.helper', {
320
+ total: totalActivity.toLocaleString('pt-BR'),
321
+ }),
322
+ icon: Activity,
323
+ },
324
+ ];
325
+
326
+ const handleSearch = () => {
327
+ setSearchQuery(searchInput);
328
+ setPeriodo(periodoInput);
329
+ };
330
+
331
+ const handleExportPdf = async () => {
332
+ if (!reportsData) {
333
+ toast.error(t('toasts.noDataToExport'));
334
+ return;
335
+ }
336
+
337
+ try {
338
+ setIsExporting(true);
339
+
340
+ const [{ default: jsPDF }, { default: autoTable }] = await Promise.all([
341
+ import('jspdf'),
342
+ import('jspdf-autotable'),
343
+ ]);
344
+
345
+ const doc = new jsPDF({ unit: 'pt', format: 'a4' }) as JsPdfWithAutoTable;
346
+ const generatedAt = new Date().toLocaleString('pt-BR');
347
+
348
+ doc.setFontSize(16);
349
+ doc.text(t('export.title'), 40, 40);
350
+ doc.setFontSize(10);
351
+ doc.text(`${t('export.periodLabel')}: ${t(`period.${periodo}`)}`, 40, 58);
352
+ doc.text(`${t('export.generatedAt')}: ${generatedAt}`, 40, 72);
353
+
354
+ autoTable(doc, {
355
+ startY: 90,
356
+ head: [[t('table.metric'), t('table.value')]],
357
+ body: [
358
+ [
359
+ t('kpis.totalStudents.title'),
360
+ `${reportsData.kpis.totalStudents.value}`,
361
+ ],
362
+ [
363
+ t('kpis.enrollments.title'),
364
+ `${reportsData.kpis.enrollments.value}`,
365
+ ],
366
+ [
367
+ t('kpis.completionRate.title'),
368
+ `${reportsData.kpis.completionRate.value}%`,
369
+ ],
370
+ [t('kpis.churnRate.title'), `${reportsData.kpis.churnRate.value}%`],
371
+ ],
372
+ theme: 'striped',
373
+ styles: { fontSize: 9 },
374
+ });
375
+
376
+ const kpiTableY = doc.lastAutoTable?.finalY ?? 90;
377
+ autoTable(doc, {
378
+ startY: kpiTableY + 16,
379
+ head: [[t('table.course'), t('table.students'), t('table.score')]],
380
+ body: filteredCourseRanking
381
+ .slice(0, 5)
382
+ .map((course) => [
383
+ course.nome,
384
+ `${course.alunos}`,
385
+ `${course.nota.toFixed(1)}`,
386
+ ]),
387
+ theme: 'grid',
388
+ styles: { fontSize: 8 },
389
+ });
390
+
391
+ const rankingTableY = doc.lastAutoTable?.finalY ?? kpiTableY + 16;
392
+ autoTable(doc, {
393
+ startY: rankingTableY + 16,
394
+ head: [[t('table.status'), t('table.students')]],
395
+ body: filteredStudentStatus.map((item) => [
396
+ t(`statusDistribution.${item.nome}`),
397
+ `${item.valor}`,
398
+ ]),
399
+ theme: 'grid',
400
+ styles: { fontSize: 8 },
401
+ });
402
+
403
+ doc.save(
404
+ `lms-reports-dashboard-${periodo}-${new Date().toISOString().slice(0, 10)}.pdf`
405
+ );
406
+ toast.success(t('toasts.exported'));
407
+ } catch (error) {
408
+ console.error('Error exporting PDF report:', error);
409
+ toast.error(t('toasts.exportError'));
410
+ } finally {
411
+ setIsExporting(false);
412
+ }
132
413
  };
133
414
 
415
+ const renderEmptyState = (className = 'min-h-[180px]') => (
416
+ <div
417
+ className={`flex flex-col items-center justify-center rounded-lg border border-dashed bg-muted/20 px-6 py-10 text-center ${className}`}
418
+ >
419
+ <p className="text-sm font-semibold">{t('emptyState.title')}</p>
420
+ <p className="mt-1 max-w-md text-sm text-muted-foreground">
421
+ {t('emptyState.description')}
422
+ </p>
423
+ </div>
424
+ );
425
+
134
426
  return (
135
427
  <Page>
136
428
  <PageHeader
137
429
  title={t('title')}
138
430
  description={t('description')}
139
431
  breadcrumbs={[
140
- {
141
- label: t('breadcrumbs.home'),
142
- href: '/',
143
- },
144
- {
145
- label: t('breadcrumbs.reports'),
146
- },
432
+ { label: t('breadcrumbs.home'), href: '/' },
433
+ { label: t('breadcrumbs.reports') },
147
434
  ]}
148
435
  actions={
149
- <div className="flex items-center gap-3">
150
- <Select value={periodo} onValueChange={setPeriodo}>
151
- <SelectTrigger className="w-[140px]">
152
- <SelectValue />
153
- </SelectTrigger>
154
- <SelectContent>
155
- <SelectItem value="7d">{t('period.7d')}</SelectItem>
156
- <SelectItem value="30d">{t('period.30d')}</SelectItem>
157
- <SelectItem value="6m">{t('period.6m')}</SelectItem>
158
- <SelectItem value="12m">{t('period.12m')}</SelectItem>
159
- </SelectContent>
160
- </Select>
161
- <Button
162
- variant="outline"
163
- className="gap-2"
164
- onClick={() => toast.success(t('toasts.exported'))}
165
- >
166
- <Download className="size-4" />
167
- <span className="hidden sm:inline">{t('actions.export')}</span>
168
- </Button>
169
- </div>
436
+ <Button
437
+ variant="outline"
438
+ className="gap-2"
439
+ onClick={handleExportPdf}
440
+ disabled={isExporting || loading}
441
+ >
442
+ <Download className="size-4" />
443
+ <span className="hidden sm:inline">{t('actions.export')}</span>
444
+ </Button>
170
445
  }
171
446
  />
172
447
 
173
448
  <motion.div
174
449
  initial={{ opacity: 0, y: 20 }}
175
450
  animate={{ opacity: 1, y: 0 }}
176
- transition={{ duration: 0.4 }}
451
+ transition={{ duration: 0.35 }}
452
+ className="space-y-4"
177
453
  >
178
- {/* KPIs */}
179
454
  {loading ? (
180
- <div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
455
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
181
456
  {Array.from({ length: 4 }).map((_, i) => (
182
457
  <Card key={i}>
183
458
  <CardContent className="p-6">
184
- <Skeleton className="mb-2 h-4 w-24" />
185
- <Skeleton className="mb-1 h-8 w-20" />
186
- <Skeleton className="h-4 w-16" />
459
+ <Skeleton className="mb-3 h-4 w-28" />
460
+ <Skeleton className="mb-2 h-8 w-24" />
461
+ <Skeleton className="h-4 w-28" />
187
462
  </CardContent>
188
463
  </Card>
189
464
  ))}
190
465
  </div>
191
466
  ) : (
192
- <div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
193
- {[
194
- {
195
- titulo: t('kpis.totalRevenue.title'),
196
- valor: 'R$ 880.000',
197
- variacao: '+18.2%',
198
- positivo: true,
199
- },
200
- {
201
- titulo: t('kpis.enrollments.title'),
202
- valor: '2.850',
203
- variacao: '+22.5%',
204
- positivo: true,
205
- },
206
- {
207
- titulo: t('kpis.completionRate.title'),
208
- valor: '78.3%',
209
- variacao: '+2.1%',
210
- positivo: true,
211
- },
212
- {
213
- titulo: t('kpis.churnRate.title'),
214
- valor: '5.4%',
215
- variacao: '-1.2%',
216
- positivo: true,
217
- },
218
- ].map((kpi, i) => (
219
- <motion.div
220
- key={kpi.titulo}
221
- initial={{ opacity: 0, y: 20 }}
222
- animate={{ opacity: 1, y: 0 }}
223
- transition={{ delay: i * 0.1 }}
224
- >
225
- <Card className="transition-shadow hover:shadow-md">
226
- <CardContent className="p-6">
227
- <p className="text-sm font-medium text-muted-foreground">
228
- {kpi.titulo}
229
- </p>
230
- <p className="mt-2 text-2xl font-bold">{kpi.valor}</p>
231
- <div className="mt-1 flex items-center gap-1">
232
- {kpi.positivo ? (
233
- <TrendingUp className="size-3 text-emerald-600" />
234
- ) : (
235
- <TrendingDown className="size-3 text-red-500" />
236
- )}
237
- <span
238
- className={`text-xs font-medium ${kpi.positivo ? 'text-emerald-600' : 'text-red-500'}`}
239
- >
240
- {kpi.variacao}
241
- </span>
242
- <span className="text-xs text-muted-foreground">
243
- {t('kpis.vsPreviousPeriod')}
244
- </span>
245
- </div>
246
- </CardContent>
247
- </Card>
248
- </motion.div>
249
- ))}
250
- </div>
467
+ <StatsCards stats={statsCards} className="gap-4" />
251
468
  )}
252
469
 
253
- {/* Tabs */}
254
- <Tabs value={activeTab} onValueChange={setActiveTab} className="mb-8">
255
- <TabsList className="mb-6">
256
- <TabsTrigger value="geral">{t('tabs.overview')}</TabsTrigger>
257
- <TabsTrigger value="cursos">{t('tabs.courses')}</TabsTrigger>
258
- <TabsTrigger value="alunos">{t('tabs.students')}</TabsTrigger>
259
- </TabsList>
260
-
261
- {/* Tab: Geral */}
262
- <TabsContent value="geral" className="mt-0">
263
- {loading ? (
264
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
470
+ <SearchBar
471
+ searchQuery={searchInput}
472
+ onSearchChange={setSearchInput}
473
+ onSearch={handleSearch}
474
+ className="mt-0"
475
+ placeholder={t('filters.searchPlaceholder')}
476
+ controls={[
477
+ {
478
+ id: 'period-filter',
479
+ type: 'select',
480
+ value: periodoInput,
481
+ onChange: setPeriodoInput,
482
+ placeholder: t('filters.period'),
483
+ options: [
484
+ { value: '7d', label: t('period.7d') },
485
+ { value: '30d', label: t('period.30d') },
486
+ { value: '6m', label: t('period.6m') },
487
+ { value: '12m', label: t('period.12m') },
488
+ ],
489
+ },
490
+ ]}
491
+ />
492
+
493
+ <div className="grid gap-4">
494
+ {loading ? (
495
+ <>
496
+ <Card>
497
+ <CardContent className="p-6">
498
+ <Skeleton className="mb-4 h-5 w-44" />
499
+ <Skeleton className="h-[240px] w-full" />
500
+ </CardContent>
501
+ </Card>
502
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
265
503
  {Array.from({ length: 4 }).map((_, i) => (
266
504
  <Card key={i}>
267
- <CardContent className="p-6">
268
- <Skeleton className="mb-4 h-5 w-40" />
269
- <Skeleton className="h-[260px] w-full" />
505
+ <CardContent className="p-5">
506
+ <Skeleton className="mb-3 h-4 w-24" />
507
+ <Skeleton className="mb-2 h-6 w-32" />
508
+ <Skeleton className="h-4 w-full" />
270
509
  </CardContent>
271
510
  </Card>
272
511
  ))}
273
512
  </div>
274
- ) : (
275
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
276
- {/* Matriculas ao longo do tempo */}
277
- <motion.div
278
- initial={{ opacity: 0, y: 20 }}
279
- animate={{ opacity: 1, y: 0 }}
280
- transition={{ delay: 0.2 }}
281
- >
282
- <Card>
283
- <CardHeader>
284
- <CardTitle className="text-base">
285
- {t('charts.monthlyEnrollments.title')}
286
- </CardTitle>
287
- <CardDescription>
288
- {t('charts.monthlyEnrollments.description')}
289
- </CardDescription>
290
- </CardHeader>
291
- <CardContent>
292
- <ResponsiveContainer width="100%" height={260}>
293
- <AreaChart data={monthlyEnrollments}>
294
- <CartesianGrid
295
- strokeDasharray="3 3"
296
- stroke="hsl(var(--border))"
297
- />
298
- <XAxis
299
- dataKey="mes"
300
- tickFormatter={(value) => t(`months.${value}`)}
301
- fontSize={12}
302
- tickLine={false}
303
- axisLine={false}
304
- />
305
- <YAxis
306
- fontSize={12}
307
- tickLine={false}
308
- axisLine={false}
309
- />
310
- <Tooltip contentStyle={tooltipStyle} />
311
- <defs>
312
- <linearGradient
313
- id="colorMatriculas"
314
- x1="0"
315
- y1="0"
316
- x2="0"
317
- y2="1"
318
- >
319
- <stop
320
- offset="5%"
321
- stopColor="#22c55e"
322
- stopOpacity={0.3}
323
- />
324
- <stop
325
- offset="95%"
326
- stopColor="#22c55e"
327
- stopOpacity={0}
328
- />
329
- </linearGradient>
330
- <linearGradient
331
- id="colorCancelamentos"
332
- x1="0"
333
- y1="0"
334
- x2="0"
335
- y2="1"
336
- >
337
- <stop
338
- offset="5%"
339
- stopColor="#ef4444"
340
- stopOpacity={0.3}
341
- />
342
- <stop
343
- offset="95%"
344
- stopColor="#ef4444"
345
- stopOpacity={0}
346
- />
347
- </linearGradient>
348
- </defs>
349
- <Area
350
- type="monotone"
351
- dataKey="matriculas"
352
- stroke="#22c55e"
353
- fill="url(#colorMatriculas)"
354
- strokeWidth={2.5}
355
- name={t('charts.monthlyEnrollments.enrollments')}
356
- />
357
- <Area
358
- type="monotone"
359
- dataKey="cancelamentos"
360
- stroke="#ef4444"
361
- fill="url(#colorCancelamentos)"
362
- strokeWidth={2}
363
- name={t('charts.monthlyEnrollments.cancellations')}
364
- />
365
- </AreaChart>
366
- </ResponsiveContainer>
367
- </CardContent>
368
- </Card>
369
- </motion.div>
370
-
371
- {/* Receita */}
372
- <motion.div
373
- initial={{ opacity: 0, y: 20 }}
374
- animate={{ opacity: 1, y: 0 }}
375
- transition={{ delay: 0.3 }}
376
- >
377
- <Card>
378
- <CardHeader>
379
- <CardTitle className="text-base">
380
- {t('charts.monthlyRevenue.title')}
381
- </CardTitle>
382
- <CardDescription>
383
- {t('charts.monthlyRevenue.description')}
384
- </CardDescription>
385
- </CardHeader>
386
- <CardContent>
387
- <ResponsiveContainer width="100%" height={260}>
388
- <BarChart data={monthlyEnrollments}>
389
- <CartesianGrid
390
- strokeDasharray="3 3"
391
- stroke="hsl(var(--border))"
392
- />
393
- <XAxis
394
- dataKey="mes"
395
- tickFormatter={(value) => t(`months.${value}`)}
396
- fontSize={12}
397
- tickLine={false}
398
- axisLine={false}
399
- />
400
- <YAxis
401
- fontSize={12}
402
- tickLine={false}
403
- axisLine={false}
404
- tickFormatter={(v) => `${(v / 1000).toFixed(0)}k`}
405
- />
406
- <Tooltip
407
- contentStyle={tooltipStyle}
408
- formatter={(value: number) => [
409
- `R$ ${value.toLocaleString('pt-BR')}`,
410
- t('charts.monthlyRevenue.revenue'),
411
- ]}
412
- />
413
- <defs>
414
- <linearGradient
415
- id="colorReceita"
416
- x1="0"
417
- y1="0"
418
- x2="0"
419
- y2="1"
420
- >
421
- <stop offset="0%" stopColor="#3b82f6" />
422
- <stop offset="100%" stopColor="#60a5fa" />
423
- </linearGradient>
424
- </defs>
425
- <Bar
426
- dataKey="receita"
427
- fill="url(#colorReceita)"
428
- radius={[6, 6, 0, 0]}
429
- name={t('charts.monthlyRevenue.revenue')}
430
- />
431
- </BarChart>
432
- </ResponsiveContainer>
433
- </CardContent>
434
- </Card>
435
- </motion.div>
436
-
437
- {/* Status dos alunos (pie) */}
438
- <motion.div
439
- initial={{ opacity: 0, y: 20 }}
440
- animate={{ opacity: 1, y: 0 }}
441
- transition={{ delay: 0.4 }}
442
- >
443
- <Card>
444
- <CardHeader>
445
- <CardTitle className="text-base">
446
- {t('charts.studentStatus.title')}
447
- </CardTitle>
448
- <CardDescription>
449
- {t('charts.studentStatus.description')}
450
- </CardDescription>
451
- </CardHeader>
452
- <CardContent className="flex flex-col items-center">
453
- <ResponsiveContainer width="100%" height={220}>
454
- <PieChart>
455
- <Pie
456
- data={statusDistribution}
457
- cx="50%"
458
- cy="50%"
459
- innerRadius={55}
460
- outerRadius={85}
461
- dataKey="valor"
462
- nameKey="nome"
463
- strokeWidth={2}
464
- stroke="hsl(var(--background))"
513
+ </>
514
+ ) : (
515
+ <>
516
+ <Card>
517
+ <CardHeader>
518
+ <CardTitle>{t('charts.monthlyOverview.title')}</CardTitle>
519
+ <CardDescription>
520
+ {t('charts.monthlyOverview.description')}
521
+ </CardDescription>
522
+ </CardHeader>
523
+ <CardContent className="pt-0">
524
+ {monthlyData.length === 0 ? (
525
+ renderEmptyState('min-h-[200px]')
526
+ ) : (
527
+ <ResponsiveContainer width="100%" height={240}>
528
+ <AreaChart data={monthlyData}>
529
+ <CartesianGrid
530
+ strokeDasharray="3 3"
531
+ stroke="hsl(var(--border))"
532
+ />
533
+ <XAxis
534
+ dataKey="mes"
535
+ tickFormatter={(value) => t(`months.${value}`)}
536
+ fontSize={12}
537
+ tickLine={false}
538
+ axisLine={false}
539
+ />
540
+ <YAxis
541
+ fontSize={12}
542
+ tickLine={false}
543
+ axisLine={false}
544
+ />
545
+ <Tooltip contentStyle={tooltipStyle} />
546
+ <defs>
547
+ <linearGradient
548
+ id="overviewEnrollments"
549
+ x1="0"
550
+ y1="0"
551
+ x2="0"
552
+ y2="1"
465
553
  >
466
- {statusDistribution.map((entry, index) => (
467
- <Cell key={`cell-${index}`} fill={entry.cor} />
468
- ))}
469
- </Pie>
470
- <Tooltip contentStyle={tooltipStyle} />
471
- </PieChart>
472
- </ResponsiveContainer>
473
- <div className="mt-2 flex flex-wrap justify-center gap-4">
474
- {statusDistribution.map((item) => (
475
- <div
476
- key={item.nome}
477
- className="flex items-center gap-1.5 text-xs"
554
+ <stop
555
+ offset="5%"
556
+ stopColor="#22c55e"
557
+ stopOpacity={0.32}
558
+ />
559
+ <stop
560
+ offset="95%"
561
+ stopColor="#22c55e"
562
+ stopOpacity={0}
563
+ />
564
+ </linearGradient>
565
+ <linearGradient
566
+ id="overviewCancellations"
567
+ x1="0"
568
+ y1="0"
569
+ x2="0"
570
+ y2="1"
478
571
  >
479
- <span
480
- className="inline-block size-2.5 rounded-full"
481
- style={{ backgroundColor: item.cor }}
572
+ <stop
573
+ offset="5%"
574
+ stopColor="#ef4444"
575
+ stopOpacity={0.28}
482
576
  />
483
- <span className="text-muted-foreground">
484
- {t(`statusDistribution.${item.nome}`)} (
485
- {item.valor})
486
- </span>
487
- </div>
488
- ))}
489
- </div>
490
- </CardContent>
491
- </Card>
492
- </motion.div>
493
-
494
- {/* Radar */}
495
- <motion.div
496
- initial={{ opacity: 0, y: 20 }}
497
- animate={{ opacity: 1, y: 0 }}
498
- transition={{ delay: 0.5 }}
499
- >
500
- <Card>
501
- <CardHeader>
502
- <CardTitle className="text-base">
503
- {t('charts.qualityIndicators.title')}
504
- </CardTitle>
505
- <CardDescription>
506
- {t('charts.qualityIndicators.description')}
507
- </CardDescription>
508
- </CardHeader>
509
- <CardContent>
510
- <ResponsiveContainer width="100%" height={260}>
511
- <RadarChart data={radarData}>
512
- <PolarGrid stroke="hsl(var(--border))" />
513
- <PolarAngleAxis
514
- dataKey="subject"
515
- fontSize={11}
516
- tickFormatter={(value) => t(`radar.${value}`)}
517
- />
518
- <PolarRadiusAxis
519
- angle={30}
520
- domain={[0, 100]}
521
- fontSize={10}
522
- />
523
- <Radar
524
- name={t('charts.qualityIndicators.score')}
525
- dataKey="A"
526
- stroke="#a855f7"
527
- fill="#a855f7"
528
- fillOpacity={0.25}
529
- strokeWidth={2.5}
530
- />
531
- <Tooltip contentStyle={tooltipStyle} />
532
- </RadarChart>
533
- </ResponsiveContainer>
534
- </CardContent>
535
- </Card>
536
- </motion.div>
537
- </div>
538
- )}
539
- </TabsContent>
540
-
541
- {/* Tab: Cursos */}
542
- <TabsContent value="cursos" className="mt-0">
543
- {loading ? (
544
- <Card>
545
- <CardContent className="p-4">
546
- {Array.from({ length: 8 }).map((_, i) => (
547
- <div
548
- key={i}
549
- className="flex items-center gap-4 border-b py-3 last:border-0"
550
- >
551
- <Skeleton className="h-4 w-48" />
552
- <Skeleton className="h-4 w-16" />
553
- <Skeleton className="h-4 w-16" />
554
- <Skeleton className="h-4 w-12" />
555
- </div>
556
- ))}
577
+ <stop
578
+ offset="95%"
579
+ stopColor="#ef4444"
580
+ stopOpacity={0}
581
+ />
582
+ </linearGradient>
583
+ </defs>
584
+ <Area
585
+ type="monotone"
586
+ dataKey="matriculas"
587
+ stroke="#22c55e"
588
+ fill="url(#overviewEnrollments)"
589
+ strokeWidth={2.5}
590
+ name={t('charts.monthlyOverview.enrollments')}
591
+ />
592
+ <Area
593
+ type="monotone"
594
+ dataKey="cancelamentos"
595
+ stroke="#ef4444"
596
+ fill="url(#overviewCancellations)"
597
+ strokeWidth={2.2}
598
+ name={t('charts.monthlyOverview.cancellations')}
599
+ />
600
+ </AreaChart>
601
+ </ResponsiveContainer>
602
+ )}
557
603
  </CardContent>
558
604
  </Card>
559
- ) : (
560
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-5">
561
- <motion.div
562
- className="lg:col-span-3"
563
- initial={{ opacity: 0, y: 20 }}
564
- animate={{ opacity: 1, y: 0 }}
565
- transition={{ delay: 0.2 }}
566
- >
567
- <Card>
568
- <CardHeader>
569
- <CardTitle className="text-base">
570
- {t('charts.courseRanking.title')}
571
- </CardTitle>
572
- <CardDescription>
573
- {t('charts.courseRanking.description')}
574
- </CardDescription>
575
- </CardHeader>
576
- <CardContent className="p-0">
577
- <Table>
578
- <TableHeader>
579
- <TableRow>
580
- <TableHead className="w-[30px]">#</TableHead>
581
- <TableHead>{t('table.course')}</TableHead>
582
- <TableHead>{t('table.students')}</TableHead>
583
- <TableHead className="hidden sm:table-cell">
584
- {t('table.completion')}
585
- </TableHead>
586
- <TableHead>{t('table.score')}</TableHead>
587
- </TableRow>
588
- </TableHeader>
589
- <TableBody>
590
- {courseRanking
591
- .sort((a, b) => b.alunos - a.alunos)
592
- .map((curso, i) => (
593
- <TableRow key={curso.nome}>
594
- <TableCell className="font-mono text-xs text-muted-foreground">
595
- {i + 1}
596
- </TableCell>
597
- <TableCell className="font-medium">
598
- {t(`courseRanking.${curso.nome}`)}
599
- </TableCell>
600
- <TableCell>{curso.alunos}</TableCell>
601
- <TableCell className="hidden sm:table-cell">
602
- <Badge
603
- variant={
604
- curso.conclusao >= 80
605
- ? 'default'
606
- : 'secondary'
607
- }
608
- >
609
- {curso.conclusao}%
610
- </Badge>
611
- </TableCell>
612
- <TableCell>
613
- <span
614
- className={
615
- curso.nota >= 8
616
- ? 'font-medium text-emerald-600'
617
- : 'text-foreground'
618
- }
619
- >
620
- {curso.nota.toFixed(1)}
621
- </span>
622
- </TableCell>
623
- </TableRow>
624
- ))}
625
- </TableBody>
626
- </Table>
627
- </CardContent>
628
- </Card>
629
- </motion.div>
630
605
 
631
- <motion.div
632
- className="lg:col-span-2"
633
- initial={{ opacity: 0, y: 20 }}
634
- animate={{ opacity: 1, y: 0 }}
635
- transition={{ delay: 0.3 }}
636
- >
637
- <Card>
638
- <CardHeader>
639
- <CardTitle className="text-base">
640
- {t('charts.performanceByArea.title')}
641
- </CardTitle>
642
- <CardDescription>
643
- {t('charts.performanceByArea.description')}
644
- </CardDescription>
645
- </CardHeader>
646
- <CardContent>
647
- <ResponsiveContainer width="100%" height={300}>
648
- <BarChart data={categoryPerformance} layout="vertical">
649
- <CartesianGrid
650
- strokeDasharray="3 3"
651
- stroke="hsl(var(--border))"
652
- />
653
- <XAxis
654
- type="number"
655
- fontSize={12}
656
- tickLine={false}
657
- axisLine={false}
658
- domain={[0, 100]}
659
- />
660
- <YAxis
661
- type="category"
662
- dataKey="area"
663
- tickFormatter={(value) => t(`areas.${value}`)}
664
- fontSize={12}
665
- tickLine={false}
666
- axisLine={false}
667
- width={80}
668
- />
669
- <Tooltip contentStyle={tooltipStyle} />
670
- <defs>
671
- <linearGradient
672
- id="colorConclusao"
673
- x1="0"
674
- y1="0"
675
- x2="1"
676
- y2="0"
677
- >
678
- <stop offset="0%" stopColor="#22c55e" />
679
- <stop offset="100%" stopColor="#4ade80" />
680
- </linearGradient>
681
- </defs>
682
- <Bar
683
- dataKey="conclusao"
684
- fill="url(#colorConclusao)"
685
- radius={[0, 6, 6, 0]}
686
- name={t('charts.performanceByArea.completion')}
687
- />
688
- </BarChart>
689
- </ResponsiveContainer>
690
- </CardContent>
691
- </Card>
692
- </motion.div>
693
- </div>
694
- )}
695
- </TabsContent>
696
-
697
- {/* Tab: Alunos */}
698
- <TabsContent value="alunos" className="mt-0">
699
- {loading ? (
700
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
701
- {Array.from({ length: 2 }).map((_, i) => (
702
- <Card key={i}>
703
- <CardContent className="p-6">
704
- <Skeleton className="mb-4 h-5 w-40" />
705
- <Skeleton className="h-[260px] w-full" />
606
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
607
+ {highlightCards.map((item) => (
608
+ <Card key={item.title}>
609
+ <CardContent className="p-5">
610
+ <div className="flex items-start justify-between gap-3">
611
+ <div className="space-y-1">
612
+ <p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
613
+ {item.title}
614
+ </p>
615
+ <p className="text-sm text-muted-foreground">
616
+ {item.description}
617
+ </p>
618
+ </div>
619
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted">
620
+ <item.icon className="size-4 text-foreground" />
621
+ </div>
622
+ </div>
623
+ <p className="mt-4 line-clamp-2 text-lg font-semibold leading-tight">
624
+ {item.value}
625
+ </p>
626
+ <p className="mt-1 text-sm text-muted-foreground">
627
+ {item.helper}
628
+ </p>
706
629
  </CardContent>
707
630
  </Card>
708
631
  ))}
709
632
  </div>
710
- ) : (
711
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
712
- <motion.div
713
- initial={{ opacity: 0, y: 20 }}
714
- animate={{ opacity: 1, y: 0 }}
715
- transition={{ delay: 0.2 }}
716
- >
717
- <Card>
718
- <CardHeader>
719
- <CardTitle className="text-base">
720
- {t('charts.weeklyActivity.title')}
721
- </CardTitle>
722
- <CardDescription>
723
- {t('charts.weeklyActivity.description')}
724
- </CardDescription>
725
- </CardHeader>
726
- <CardContent>
727
- <ResponsiveContainer width="100%" height={280}>
728
- <BarChart data={weeklyActivity}>
729
- <CartesianGrid
730
- strokeDasharray="3 3"
731
- stroke="hsl(var(--border))"
732
- />
733
- <XAxis
734
- dataKey="dia"
735
- tickFormatter={(value) => t(`weekdays.${value}`)}
736
- fontSize={12}
737
- tickLine={false}
738
- axisLine={false}
739
- />
740
- <YAxis
741
- fontSize={12}
742
- tickLine={false}
743
- axisLine={false}
744
- />
745
- <Tooltip contentStyle={tooltipStyle} />
746
- <Bar
747
- dataKey="acessos"
748
- fill="#3b82f6"
749
- radius={[4, 4, 0, 0]}
750
- name={t('charts.weeklyActivity.accesses')}
751
- />
752
- <Bar
753
- dataKey="aulas"
754
- fill="#22c55e"
755
- radius={[4, 4, 0, 0]}
756
- name={t('charts.weeklyActivity.classes')}
757
- />
758
- <Bar
759
- dataKey="exercicios"
760
- fill="#f97316"
761
- radius={[4, 4, 0, 0]}
762
- name={t('charts.weeklyActivity.exercises')}
763
- />
764
- </BarChart>
765
- </ResponsiveContainer>
766
- </CardContent>
767
- </Card>
768
- </motion.div>
633
+ </>
634
+ )}
635
+ </div>
769
636
 
770
- <motion.div
771
- initial={{ opacity: 0, y: 20 }}
772
- animate={{ opacity: 1, y: 0 }}
773
- transition={{ delay: 0.3 }}
774
- >
775
- <Card>
776
- <CardHeader>
777
- <CardTitle className="text-base">
778
- {t('charts.performanceEvolution.title')}
779
- </CardTitle>
780
- <CardDescription>
781
- {t('charts.performanceEvolution.description')}
782
- </CardDescription>
783
- </CardHeader>
784
- <CardContent>
785
- <ResponsiveContainer width="100%" height={280}>
786
- <LineChart
787
- data={monthlyEnrollments.map((m, i) => ({
788
- mes: m.mes,
789
- nota: 6.8 + Math.sin(i * 0.5) * 0.5 + i * 0.1,
790
- }))}
791
- >
792
- <CartesianGrid
793
- strokeDasharray="3 3"
794
- stroke="hsl(var(--border))"
795
- />
796
- <XAxis
797
- dataKey="mes"
798
- tickFormatter={(value) => t(`months.${value}`)}
799
- fontSize={12}
800
- tickLine={false}
801
- axisLine={false}
802
- />
803
- <YAxis
804
- fontSize={12}
805
- tickLine={false}
806
- axisLine={false}
807
- domain={[6, 10]}
808
- />
809
- <Tooltip contentStyle={tooltipStyle} />
810
- <Line
811
- type="monotone"
812
- dataKey="nota"
813
- stroke="#a855f7"
814
- strokeWidth={2.5}
815
- dot={{
816
- r: 4,
817
- fill: '#a855f7',
818
- strokeWidth: 2,
819
- stroke: '#fff',
820
- }}
821
- activeDot={{ r: 6, fill: '#a855f7' }}
822
- name={t('charts.performanceEvolution.averageScore')}
823
- />
824
- </LineChart>
825
- </ResponsiveContainer>
826
- </CardContent>
827
- </Card>
828
- </motion.div>
829
-
830
- {/* Performance por Area */}
831
- <motion.div
832
- className="lg:col-span-2"
833
- initial={{ opacity: 0, y: 20 }}
834
- animate={{ opacity: 1, y: 0 }}
835
- transition={{ delay: 0.4 }}
836
- >
837
- <Card>
838
- <CardHeader>
839
- <CardTitle className="text-base">
840
- {t('charts.metricsByArea.title')}
841
- </CardTitle>
842
- <CardDescription>
843
- {t('charts.metricsByArea.description')}
844
- </CardDescription>
845
- </CardHeader>
846
- <CardContent className="p-0">
847
- <Table>
848
- <TableHeader>
849
- <TableRow>
850
- <TableHead>{t('table.area')}</TableHead>
851
- <TableHead>{t('table.students')}</TableHead>
852
- <TableHead>{t('table.averageScore')}</TableHead>
853
- <TableHead>{t('table.completionRate')}</TableHead>
854
- <TableHead className="hidden sm:table-cell">
855
- {t('table.trend')}
856
- </TableHead>
857
- </TableRow>
858
- </TableHeader>
859
- <TableBody>
860
- {categoryPerformance.map((area) => (
861
- <TableRow key={area.area}>
637
+ <div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
638
+ {loading ? (
639
+ <>
640
+ <Card>
641
+ <CardContent className="p-6">
642
+ <Skeleton className="mb-4 h-5 w-40" />
643
+ <Skeleton className="h-[300px] w-full" />
644
+ </CardContent>
645
+ </Card>
646
+ <Card>
647
+ <CardContent className="p-6">
648
+ <Skeleton className="mb-4 h-5 w-40" />
649
+ <Skeleton className="h-[300px] w-full" />
650
+ </CardContent>
651
+ </Card>
652
+ </>
653
+ ) : (
654
+ <>
655
+ <Card>
656
+ <CardHeader>
657
+ <CardTitle>{t('charts.topCourses.title')}</CardTitle>
658
+ <CardDescription>
659
+ {t('charts.topCourses.description')}
660
+ </CardDescription>
661
+ </CardHeader>
662
+ <CardContent className="p-0">
663
+ {filteredCourseRanking.length === 0 ? (
664
+ <div className="px-6 pb-6">
665
+ {renderEmptyState('min-h-[220px]')}
666
+ </div>
667
+ ) : (
668
+ <Table>
669
+ <TableHeader>
670
+ <TableRow>
671
+ <TableHead className="w-[44px]">#</TableHead>
672
+ <TableHead>{t('table.course')}</TableHead>
673
+ <TableHead>{t('table.students')}</TableHead>
674
+ <TableHead>{t('table.score')}</TableHead>
675
+ </TableRow>
676
+ </TableHeader>
677
+ <TableBody>
678
+ {filteredCourseRanking
679
+ .slice(0, 5)
680
+ .map((item, index) => (
681
+ <TableRow key={item.id}>
682
+ <TableCell className="font-mono text-xs text-muted-foreground">
683
+ {index + 1}
684
+ </TableCell>
862
685
  <TableCell className="font-medium">
863
- {t(`areas.${area.area}`)}
686
+ {item.nome}
864
687
  </TableCell>
865
- <TableCell>{area.alunos}</TableCell>
688
+ <TableCell>{item.alunos}</TableCell>
866
689
  <TableCell>
867
690
  <span
868
691
  className={
869
- area.media >= 8
692
+ item.nota >= 8
870
693
  ? 'font-medium text-emerald-600'
871
694
  : 'text-foreground'
872
695
  }
873
696
  >
874
- {area.media.toFixed(1)}
697
+ {item.nota.toFixed(1)}
875
698
  </span>
876
699
  </TableCell>
877
- <TableCell>
878
- <Badge
879
- variant={
880
- area.conclusao >= 80
881
- ? 'default'
882
- : 'secondary'
883
- }
884
- >
885
- {area.conclusao}%
886
- </Badge>
887
- </TableCell>
888
- <TableCell className="hidden sm:table-cell">
889
- <div className="flex items-center gap-1 text-emerald-600">
890
- <ArrowUpRight className="size-3" />
891
- <span className="text-xs font-medium">
892
- {t('table.growing')}
893
- </span>
894
- </div>
895
- </TableCell>
896
700
  </TableRow>
897
701
  ))}
898
- </TableBody>
899
- </Table>
900
- </CardContent>
901
- </Card>
902
- </motion.div>
702
+ </TableBody>
703
+ </Table>
704
+ )}
705
+ </CardContent>
706
+ </Card>
707
+
708
+ <Card>
709
+ <CardHeader>
710
+ <CardTitle>{t('charts.studentStatus.title')}</CardTitle>
711
+ <CardDescription>
712
+ {t('charts.studentStatus.description')}
713
+ </CardDescription>
714
+ </CardHeader>
715
+ <CardContent className="flex h-full pt-0">
716
+ {filteredStudentStatus.length === 0 ? (
717
+ renderEmptyState('min-h-[220px] w-full')
718
+ ) : (
719
+ <ResponsiveContainer width="100%" height={300}>
720
+ <PieChart>
721
+ <Pie
722
+ data={filteredStudentStatus}
723
+ dataKey="valor"
724
+ nameKey="nome"
725
+ innerRadius={60}
726
+ outerRadius={95}
727
+ strokeWidth={2}
728
+ stroke="hsl(var(--background))"
729
+ >
730
+ {filteredStudentStatus.map((entry, index) => (
731
+ <Cell
732
+ key={`${entry.nome}-${index}`}
733
+ fill={entry.cor}
734
+ />
735
+ ))}
736
+ </Pie>
737
+ <Tooltip contentStyle={tooltipStyle} />
738
+ </PieChart>
739
+ </ResponsiveContainer>
740
+ )}
741
+ </CardContent>
742
+ </Card>
743
+ </>
744
+ )}
745
+ </div>
746
+
747
+ <div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
748
+ {loading ? (
749
+ <>
750
+ <Card>
751
+ <CardContent className="p-6">
752
+ <Skeleton className="mb-4 h-5 w-44" />
753
+ <Skeleton className="h-[260px] w-full" />
754
+ </CardContent>
755
+ </Card>
756
+ <Card>
757
+ <CardContent className="p-6">
758
+ <Skeleton className="mb-4 h-5 w-44" />
759
+ <Skeleton className="h-[260px] w-full" />
760
+ </CardContent>
761
+ </Card>
762
+ </>
763
+ ) : (
764
+ <>
765
+ <Card>
766
+ <CardHeader>
767
+ <CardTitle>{t('charts.pathPerformance.title')}</CardTitle>
768
+ <CardDescription>
769
+ {t('charts.pathPerformance.description')}
770
+ </CardDescription>
771
+ </CardHeader>
772
+ <CardContent className="pt-0">
773
+ {filteredCategoryPerformance.length === 0 ? (
774
+ renderEmptyState('min-h-[220px]')
775
+ ) : (
776
+ <ResponsiveContainer width="100%" height={260}>
777
+ <BarChart
778
+ data={filteredCategoryPerformance}
779
+ layout="vertical"
780
+ >
781
+ <CartesianGrid
782
+ strokeDasharray="3 3"
783
+ stroke="hsl(var(--border))"
784
+ />
785
+ <XAxis
786
+ type="number"
787
+ fontSize={12}
788
+ tickLine={false}
789
+ axisLine={false}
790
+ domain={[0, 100]}
791
+ />
792
+ <YAxis
793
+ type="category"
794
+ dataKey="area"
795
+ fontSize={12}
796
+ tickLine={false}
797
+ axisLine={false}
798
+ width={120}
799
+ />
800
+ <Tooltip contentStyle={tooltipStyle} />
801
+ <Bar
802
+ dataKey="conclusao"
803
+ fill="#10b981"
804
+ radius={[0, 6, 6, 0]}
805
+ name={t('charts.pathPerformance.completion')}
806
+ />
807
+ </BarChart>
808
+ </ResponsiveContainer>
809
+ )}
810
+ </CardContent>
811
+ </Card>
812
+
813
+ <Card>
814
+ <CardHeader>
815
+ <CardTitle>
816
+ {t('charts.performanceEvolution.title')}
817
+ </CardTitle>
818
+ <CardDescription>
819
+ {t('charts.performanceEvolution.description')}
820
+ </CardDescription>
821
+ </CardHeader>
822
+ <CardContent className="pt-0">
823
+ {performanceEvolution.length === 0 ? (
824
+ renderEmptyState('min-h-[220px]')
825
+ ) : (
826
+ <ResponsiveContainer width="100%" height={260}>
827
+ <BarChart data={performanceEvolution}>
828
+ <CartesianGrid
829
+ strokeDasharray="3 3"
830
+ stroke="hsl(var(--border))"
831
+ />
832
+ <XAxis
833
+ dataKey="mes"
834
+ tickFormatter={(value) => t(`months.${value}`)}
835
+ fontSize={12}
836
+ tickLine={false}
837
+ axisLine={false}
838
+ />
839
+ <YAxis
840
+ fontSize={12}
841
+ tickLine={false}
842
+ axisLine={false}
843
+ domain={[0, 10]}
844
+ />
845
+ <Tooltip contentStyle={tooltipStyle} />
846
+ <Bar
847
+ dataKey="nota"
848
+ fill="#7c3aed"
849
+ radius={[6, 6, 0, 0]}
850
+ name={t('charts.performanceEvolution.averageScore')}
851
+ />
852
+ </BarChart>
853
+ </ResponsiveContainer>
854
+ )}
855
+ </CardContent>
856
+ </Card>
857
+ </>
858
+ )}
859
+ </div>
860
+
861
+ {!loading && (
862
+ <Card>
863
+ <CardContent className="flex flex-wrap items-center justify-between gap-4 p-4">
864
+ <div className="flex items-center gap-2">
865
+ <Badge
866
+ variant="secondary"
867
+ className="gap-1.5 text-xs font-medium"
868
+ >
869
+ <span className="inline-block size-2 rounded-full bg-emerald-500" />
870
+ {t('footer.quickSummary')}
871
+ </Badge>
903
872
  </div>
904
- )}
905
- </TabsContent>
906
- </Tabs>
873
+ <div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
874
+ <span>
875
+ {t('footer.averageScore', {
876
+ total: averageScore ? averageScore.toFixed(1) : '0.0',
877
+ })}
878
+ </span>
879
+ <span>
880
+ {t('footer.totalActivity', {
881
+ total: totalActivity.toLocaleString('pt-BR'),
882
+ })}
883
+ </span>
884
+ <span>
885
+ {t('footer.tracks', {
886
+ total:
887
+ filteredCategoryPerformance.length.toLocaleString(
888
+ 'pt-BR'
889
+ ),
890
+ })}
891
+ </span>
892
+ <span>
893
+ {t('footer.courses', {
894
+ total: filteredCourseRanking.length.toLocaleString('pt-BR'),
895
+ })}
896
+ </span>
897
+ </div>
898
+ </CardContent>
899
+ </Card>
900
+ )}
907
901
  </motion.div>
908
902
  </Page>
909
903
  );