@hed-hog/lms 0.0.303 → 0.0.305

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 (462) 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-lead.dto.d.ts +4 -0
  119. package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts.map +1 -0
  120. package/dist/enterprise/dto/add-enterprise-lead.dto.js +22 -0
  121. package/dist/enterprise/dto/add-enterprise-lead.dto.js.map +1 -0
  122. package/dist/enterprise/dto/add-enterprise-student.dto.d.ts +5 -0
  123. package/dist/enterprise/dto/add-enterprise-student.dto.d.ts.map +1 -0
  124. package/dist/enterprise/dto/add-enterprise-student.dto.js +27 -0
  125. package/dist/enterprise/dto/add-enterprise-student.dto.js.map +1 -0
  126. package/dist/enterprise/dto/add-enterprise-user.dto.d.ts +7 -0
  127. package/dist/enterprise/dto/add-enterprise-user.dto.d.ts.map +1 -0
  128. package/dist/enterprise/dto/add-enterprise-user.dto.js +36 -0
  129. package/dist/enterprise/dto/add-enterprise-user.dto.js.map +1 -0
  130. package/dist/enterprise/dto/create-enterprise.dto.d.ts +10 -0
  131. package/dist/enterprise/dto/create-enterprise.dto.d.ts.map +1 -0
  132. package/dist/enterprise/dto/create-enterprise.dto.js +54 -0
  133. package/dist/enterprise/dto/create-enterprise.dto.js.map +1 -0
  134. package/dist/enterprise/dto/update-enterprise-student.dto.d.ts +4 -0
  135. package/dist/enterprise/dto/update-enterprise-student.dto.d.ts.map +1 -0
  136. package/dist/enterprise/dto/update-enterprise-student.dto.js +22 -0
  137. package/dist/enterprise/dto/update-enterprise-student.dto.js.map +1 -0
  138. package/dist/enterprise/dto/update-enterprise-user.dto.d.ts +5 -0
  139. package/dist/enterprise/dto/update-enterprise-user.dto.d.ts.map +1 -0
  140. package/dist/enterprise/dto/update-enterprise-user.dto.js +27 -0
  141. package/dist/enterprise/dto/update-enterprise-user.dto.js.map +1 -0
  142. package/dist/enterprise/dto/update-enterprise.dto.d.ts +6 -0
  143. package/dist/enterprise/dto/update-enterprise.dto.d.ts.map +1 -0
  144. package/dist/enterprise/dto/update-enterprise.dto.js +9 -0
  145. package/dist/enterprise/dto/update-enterprise.dto.js.map +1 -0
  146. package/dist/enterprise/enterprise.controller.d.ts +269 -0
  147. package/dist/enterprise/enterprise.controller.d.ts.map +1 -0
  148. package/dist/enterprise/enterprise.controller.js +311 -0
  149. package/dist/enterprise/enterprise.controller.js.map +1 -0
  150. package/dist/enterprise/enterprise.module.d.ts +3 -0
  151. package/dist/enterprise/enterprise.module.d.ts.map +1 -0
  152. package/dist/enterprise/enterprise.module.js +25 -0
  153. package/dist/enterprise/enterprise.module.js.map +1 -0
  154. package/dist/enterprise/enterprise.service.d.ts +282 -0
  155. package/dist/enterprise/enterprise.service.d.ts.map +1 -0
  156. package/dist/enterprise/enterprise.service.js +627 -0
  157. package/dist/enterprise/enterprise.service.js.map +1 -0
  158. package/dist/evaluation/evaluation.controller.d.ts +56 -0
  159. package/dist/evaluation/evaluation.controller.d.ts.map +1 -0
  160. package/dist/evaluation/evaluation.controller.js +76 -0
  161. package/dist/evaluation/evaluation.controller.js.map +1 -0
  162. package/dist/evaluation/evaluation.module.d.ts +3 -0
  163. package/dist/evaluation/evaluation.module.d.ts.map +1 -0
  164. package/dist/evaluation/evaluation.module.js +25 -0
  165. package/dist/evaluation/evaluation.module.js.map +1 -0
  166. package/dist/evaluation/evaluation.service.d.ts +67 -0
  167. package/dist/evaluation/evaluation.service.d.ts.map +1 -0
  168. package/dist/evaluation/evaluation.service.js +378 -0
  169. package/dist/evaluation/evaluation.service.js.map +1 -0
  170. package/dist/exam/dto/create-exam-question.dto.d.ts +25 -0
  171. package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -0
  172. package/dist/exam/dto/create-exam-question.dto.js +117 -0
  173. package/dist/exam/dto/create-exam-question.dto.js.map +1 -0
  174. package/dist/exam/dto/create-exam.dto.d.ts +11 -0
  175. package/dist/exam/dto/create-exam.dto.d.ts.map +1 -0
  176. package/dist/exam/dto/create-exam.dto.js +63 -0
  177. package/dist/exam/dto/create-exam.dto.js.map +1 -0
  178. package/dist/exam/dto/reorder-exam-questions.dto.d.ts +4 -0
  179. package/dist/exam/dto/reorder-exam-questions.dto.d.ts.map +1 -0
  180. package/dist/exam/dto/reorder-exam-questions.dto.js +23 -0
  181. package/dist/exam/dto/reorder-exam-questions.dto.js.map +1 -0
  182. package/dist/exam/dto/save-exam-attempt-answers.dto.d.ts +14 -0
  183. package/dist/exam/dto/save-exam-attempt-answers.dto.d.ts.map +1 -0
  184. package/dist/exam/dto/save-exam-attempt-answers.dto.js +68 -0
  185. package/dist/exam/dto/save-exam-attempt-answers.dto.js.map +1 -0
  186. package/dist/exam/dto/start-exam-attempt.dto.d.ts +4 -0
  187. package/dist/exam/dto/start-exam-attempt.dto.d.ts.map +1 -0
  188. package/dist/exam/dto/start-exam-attempt.dto.js +23 -0
  189. package/dist/exam/dto/start-exam-attempt.dto.js.map +1 -0
  190. package/dist/exam/dto/submit-exam-attempt.dto.d.ts +5 -0
  191. package/dist/exam/dto/submit-exam-attempt.dto.d.ts.map +1 -0
  192. package/dist/exam/dto/submit-exam-attempt.dto.js +23 -0
  193. package/dist/exam/dto/submit-exam-attempt.dto.js.map +1 -0
  194. package/dist/exam/dto/update-exam-question.dto.d.ts +6 -0
  195. package/dist/exam/dto/update-exam-question.dto.d.ts.map +1 -0
  196. package/dist/exam/dto/update-exam-question.dto.js +9 -0
  197. package/dist/exam/dto/update-exam-question.dto.js.map +1 -0
  198. package/dist/exam/dto/update-exam.dto.d.ts +6 -0
  199. package/dist/exam/dto/update-exam.dto.d.ts.map +1 -0
  200. package/dist/exam/dto/update-exam.dto.js +9 -0
  201. package/dist/exam/dto/update-exam.dto.js.map +1 -0
  202. package/dist/exam/exam-attempt.controller.d.ts +273 -0
  203. package/dist/exam/exam-attempt.controller.d.ts.map +1 -0
  204. package/dist/exam/exam-attempt.controller.js +84 -0
  205. package/dist/exam/exam-attempt.controller.js.map +1 -0
  206. package/dist/exam/exam-attempt.service.d.ts +302 -0
  207. package/dist/exam/exam-attempt.service.d.ts.map +1 -0
  208. package/dist/exam/exam-attempt.service.js +776 -0
  209. package/dist/exam/exam-attempt.service.js.map +1 -0
  210. package/dist/exam/exam.controller.d.ts +162 -0
  211. package/dist/exam/exam.controller.d.ts.map +1 -0
  212. package/dist/exam/exam.controller.js +158 -0
  213. package/dist/exam/exam.controller.js.map +1 -0
  214. package/dist/exam/exam.module.d.ts +3 -0
  215. package/dist/exam/exam.module.d.ts.map +1 -0
  216. package/dist/exam/exam.module.js +27 -0
  217. package/dist/exam/exam.module.js.map +1 -0
  218. package/dist/exam/exam.service.d.ts +179 -0
  219. package/dist/exam/exam.service.d.ts.map +1 -0
  220. package/dist/exam/exam.service.js +597 -0
  221. package/dist/exam/exam.service.js.map +1 -0
  222. package/dist/index.d.ts +28 -0
  223. package/dist/index.d.ts.map +1 -1
  224. package/dist/index.js +28 -0
  225. package/dist/index.js.map +1 -1
  226. package/dist/instructor/dto/create-instructor.dto.d.ts +10 -0
  227. package/dist/instructor/dto/create-instructor.dto.d.ts.map +1 -0
  228. package/dist/instructor/dto/create-instructor.dto.js +55 -0
  229. package/dist/instructor/dto/create-instructor.dto.js.map +1 -0
  230. package/dist/instructor/dto/update-instructor.dto.d.ts +9 -0
  231. package/dist/instructor/dto/update-instructor.dto.d.ts.map +1 -0
  232. package/dist/instructor/dto/update-instructor.dto.js +51 -0
  233. package/dist/instructor/dto/update-instructor.dto.js.map +1 -0
  234. package/dist/instructor/instructor.controller.d.ts +52 -0
  235. package/dist/instructor/instructor.controller.d.ts.map +1 -0
  236. package/dist/instructor/instructor.controller.js +98 -0
  237. package/dist/instructor/instructor.controller.js.map +1 -0
  238. package/dist/instructor/instructor.module.d.ts +3 -0
  239. package/dist/instructor/instructor.module.d.ts.map +1 -0
  240. package/dist/instructor/instructor.module.js +25 -0
  241. package/dist/instructor/instructor.module.js.map +1 -0
  242. package/dist/instructor/instructor.service.d.ts +79 -0
  243. package/dist/instructor/instructor.service.d.ts.map +1 -0
  244. package/dist/instructor/instructor.service.js +528 -0
  245. package/dist/instructor/instructor.service.js.map +1 -0
  246. package/dist/lms.module.d.ts.map +1 -1
  247. package/dist/lms.module.js +36 -4
  248. package/dist/lms.module.js.map +1 -1
  249. package/dist/reports/reports.controller.d.ts +69 -0
  250. package/dist/reports/reports.controller.d.ts.map +1 -0
  251. package/dist/reports/reports.controller.js +40 -0
  252. package/dist/reports/reports.controller.js.map +1 -0
  253. package/dist/reports/reports.module.d.ts +3 -0
  254. package/dist/reports/reports.module.d.ts.map +1 -0
  255. package/dist/reports/reports.module.js +25 -0
  256. package/dist/reports/reports.module.js.map +1 -0
  257. package/dist/reports/reports.service.d.ts +80 -0
  258. package/dist/reports/reports.service.d.ts.map +1 -0
  259. package/dist/reports/reports.service.js +366 -0
  260. package/dist/reports/reports.service.js.map +1 -0
  261. package/dist/training/dto/create-training.dto.d.ts +19 -0
  262. package/dist/training/dto/create-training.dto.d.ts.map +1 -0
  263. package/dist/training/dto/create-training.dto.js +98 -0
  264. package/dist/training/dto/create-training.dto.js.map +1 -0
  265. package/dist/training/dto/update-training.dto.d.ts +6 -0
  266. package/dist/training/dto/update-training.dto.d.ts.map +1 -0
  267. package/dist/training/dto/update-training.dto.js +9 -0
  268. package/dist/training/dto/update-training.dto.js.map +1 -0
  269. package/dist/training/training.controller.d.ts +195 -0
  270. package/dist/training/training.controller.d.ts.map +1 -0
  271. package/dist/training/training.controller.js +104 -0
  272. package/dist/training/training.controller.js.map +1 -0
  273. package/dist/training/training.module.d.ts +3 -0
  274. package/dist/training/training.module.d.ts.map +1 -0
  275. package/dist/training/training.module.js +25 -0
  276. package/dist/training/training.module.js.map +1 -0
  277. package/dist/training/training.service.d.ts +213 -0
  278. package/dist/training/training.service.d.ts.map +1 -0
  279. package/dist/training/training.service.js +497 -0
  280. package/dist/training/training.service.js.map +1 -0
  281. package/hedhog/data/dashboard.yaml +6 -0
  282. package/hedhog/data/dashboard_component.yaml +153 -0
  283. package/hedhog/data/dashboard_component_role.yaml +97 -0
  284. package/hedhog/data/dashboard_item.yaml +167 -0
  285. package/hedhog/data/dashboard_role.yaml +6 -0
  286. package/hedhog/data/instructor_qualification.yaml +16 -0
  287. package/hedhog/data/menu.yaml +129 -19
  288. package/hedhog/data/role.yaml +25 -1
  289. package/hedhog/data/route.yaml +867 -0
  290. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +1992 -0
  291. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +480 -0
  292. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  293. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +164 -0
  294. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +120 -0
  295. package/hedhog/frontend/app/_components/lms-class-calendar.tsx.ejs +272 -0
  296. package/hedhog/frontend/app/_components/mobile-calendar.tsx.ejs +277 -0
  297. package/hedhog/frontend/app/_lib/editor/canvasInstance.ts.ejs +48 -0
  298. package/hedhog/frontend/app/_lib/editor/pctHelpers.ts.ejs +50 -0
  299. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +268 -0
  300. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +94 -0
  301. package/hedhog/frontend/app/_lib/store/useTemplateStore.ts.ejs +284 -0
  302. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +638 -0
  303. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +916 -0
  304. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +200 -0
  305. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +769 -0
  306. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +104 -0
  307. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +354 -0
  308. package/hedhog/frontend/app/certificates/models/editor/page.tsx.ejs +5 -0
  309. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +883 -0
  310. package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +279 -0
  311. package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +1027 -0
  312. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +3130 -993
  313. package/hedhog/frontend/app/classes/page.tsx.ejs +2731 -759
  314. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +80 -0
  315. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +226 -0
  316. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +71 -0
  317. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +42 -0
  318. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +111 -0
  319. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +113 -0
  320. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +215 -0
  321. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +236 -0
  322. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +141 -0
  323. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +57 -0
  324. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  325. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +33 -0
  326. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +933 -1103
  327. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +699 -117
  328. package/hedhog/frontend/app/courses/page.tsx.ejs +1018 -1042
  329. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +317 -0
  330. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-panel.tsx.ejs +88 -0
  331. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +318 -0
  332. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +332 -0
  333. package/hedhog/frontend/app/enterprise/_components/enterprise-class-create-sheet.tsx.ejs +57 -0
  334. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +390 -0
  335. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +112 -0
  336. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +183 -0
  337. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +363 -0
  338. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-constants.ts.ejs +88 -0
  339. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +548 -0
  340. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-utils.ts.ejs +33 -0
  341. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +266 -0
  342. package/hedhog/frontend/app/enterprise/_components/enterprise-person-picker.ts.ejs +31 -0
  343. package/hedhog/frontend/app/enterprise/_components/enterprise-progress-bar.tsx.ejs +21 -0
  344. package/hedhog/frontend/app/enterprise/_components/enterprise-related-tab.tsx.ejs +187 -0
  345. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +397 -0
  346. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +167 -0
  347. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +267 -0
  348. package/hedhog/frontend/app/enterprise/_components/enterprise-system-user-picker.ts.ejs +42 -0
  349. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +96 -0
  350. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +207 -0
  351. package/hedhog/frontend/app/enterprise/_components/enterprise-user-distribution-chart.tsx.ejs +149 -0
  352. package/hedhog/frontend/app/enterprise/page.tsx.ejs +596 -0
  353. package/hedhog/frontend/app/evaluations/page.tsx.ejs +1250 -0
  354. package/hedhog/frontend/app/exams/[id]/attempt/page.tsx.ejs +642 -196
  355. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +11 -0
  356. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +1316 -436
  357. package/hedhog/frontend/app/exams/page.tsx.ejs +799 -546
  358. package/hedhog/frontend/app/layout.tsx.ejs +5 -0
  359. package/hedhog/frontend/app/page.tsx.ejs +5 -1222
  360. package/hedhog/frontend/app/reports/courses/page.tsx.ejs +843 -0
  361. package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +890 -0
  362. package/hedhog/frontend/app/reports/page.tsx.ejs +802 -808
  363. package/hedhog/frontend/app/reports/students/page.tsx.ejs +772 -0
  364. package/hedhog/frontend/app/training/page.tsx.ejs +1873 -628
  365. package/hedhog/frontend/messages/en.json +1606 -111
  366. package/hedhog/frontend/messages/pt.json +1636 -134
  367. package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +74 -0
  368. package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +74 -0
  369. package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +81 -0
  370. package/hedhog/frontend/widgets/category-distribution-chart.tsx.ejs +119 -0
  371. package/hedhog/frontend/widgets/class-calendar.tsx.ejs +440 -0
  372. package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +81 -0
  373. package/hedhog/frontend/widgets/engagement-chart.tsx.ejs +120 -0
  374. package/hedhog/frontend/widgets/footer-summary.tsx.ejs +80 -0
  375. package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +74 -0
  376. package/hedhog/frontend/widgets/latest-enrollments.tsx.ejs +166 -0
  377. package/hedhog/frontend/widgets/student-growth-chart.tsx.ejs +89 -0
  378. package/hedhog/frontend/widgets/top-courses-chart.tsx.ejs +104 -0
  379. package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +78 -0
  380. package/hedhog/frontend/widgets/upcoming-classes.tsx.ejs +152 -0
  381. package/hedhog/table/course.yaml +28 -10
  382. package/hedhog/table/course_class_group.yaml +8 -0
  383. package/hedhog/table/course_class_session.yaml +33 -0
  384. package/hedhog/table/course_instructor.yaml +27 -0
  385. package/hedhog/table/enterprise.yaml +29 -0
  386. package/hedhog/table/enterprise_class_group.yaml +20 -0
  387. package/hedhog/table/enterprise_course.yaml +23 -0
  388. package/hedhog/table/enterprise_student.yaml +24 -0
  389. package/hedhog/table/enterprise_user.yaml +35 -0
  390. package/hedhog/table/instructor_qualification.yaml +26 -0
  391. package/hedhog/table/instructor_qualification_assignment.yaml +22 -0
  392. package/hedhog/table/question.yaml +6 -0
  393. package/package.json +6 -6
  394. package/src/certificate/certificate.controller.ts +83 -0
  395. package/src/certificate/certificate.module.ts +13 -0
  396. package/src/certificate/certificate.service.ts +413 -0
  397. package/src/certificate/dto/create-certificate-template.dto.ts +25 -0
  398. package/src/certificate/dto/update-certificate-template.dto.ts +6 -0
  399. package/src/class-group/class-group.controller.ts +189 -0
  400. package/src/class-group/class-group.module.ts +12 -0
  401. package/src/class-group/class-group.service.ts +1802 -0
  402. package/src/class-group/dto/create-class-group.dto.ts +139 -0
  403. package/src/class-group/dto/create-session.dto.ts +102 -0
  404. package/src/class-group/dto/enrollment.dto.ts +70 -0
  405. package/src/class-group/dto/update-class-group.dto.ts +4 -0
  406. package/src/class-group/dto/update-session.dto.ts +9 -0
  407. package/src/course/course-structure.controller.ts +85 -0
  408. package/src/course/course-structure.service.ts +525 -0
  409. package/src/course/course.controller.ts +69 -0
  410. package/src/course/course.module.ts +15 -0
  411. package/src/course/course.service.ts +920 -0
  412. package/src/course/dto/create-course-structure-lesson.dto.ts +97 -0
  413. package/src/course/dto/create-course-structure-session.dto.ts +22 -0
  414. package/src/course/dto/create-course.dto.ts +111 -0
  415. package/src/course/dto/update-course-structure-lesson.dto.ts +6 -0
  416. package/src/course/dto/update-course-structure-session.dto.ts +6 -0
  417. package/src/course/dto/update-course.dto.ts +4 -0
  418. package/src/dashboard/dashboard.controller.ts +14 -0
  419. package/src/dashboard/dashboard.module.ts +12 -0
  420. package/src/dashboard/dashboard.service.ts +726 -0
  421. package/src/enterprise/dto/add-enterprise-class-group.dto.ts +7 -0
  422. package/src/enterprise/dto/add-enterprise-course.dto.ts +11 -0
  423. package/src/enterprise/dto/add-enterprise-student.dto.ts +16 -0
  424. package/src/enterprise/dto/add-enterprise-user.dto.ts +23 -0
  425. package/src/enterprise/dto/create-enterprise.dto.ts +41 -0
  426. package/src/enterprise/dto/update-enterprise-student.dto.ts +7 -0
  427. package/src/enterprise/dto/update-enterprise-user.dto.ts +11 -0
  428. package/src/enterprise/dto/update-enterprise.dto.ts +4 -0
  429. package/src/enterprise/enterprise.controller.ts +233 -0
  430. package/src/enterprise/enterprise.module.ts +12 -0
  431. package/src/enterprise/enterprise.service.ts +712 -0
  432. package/src/evaluation/evaluation.controller.ts +44 -0
  433. package/src/evaluation/evaluation.module.ts +12 -0
  434. package/src/evaluation/evaluation.service.ts +394 -0
  435. package/src/exam/dto/create-exam-question.dto.ts +103 -0
  436. package/src/exam/dto/create-exam.dto.ts +41 -0
  437. package/src/exam/dto/reorder-exam-questions.dto.ts +8 -0
  438. package/src/exam/dto/save-exam-attempt-answers.dto.ts +55 -0
  439. package/src/exam/dto/start-exam-attempt.dto.ts +8 -0
  440. package/src/exam/dto/submit-exam-attempt.dto.ts +8 -0
  441. package/src/exam/dto/update-exam-question.dto.ts +4 -0
  442. package/src/exam/dto/update-exam.dto.ts +4 -0
  443. package/src/exam/exam-attempt.controller.ts +65 -0
  444. package/src/exam/exam-attempt.service.ts +1008 -0
  445. package/src/exam/exam.controller.ts +102 -0
  446. package/src/exam/exam.module.ts +14 -0
  447. package/src/exam/exam.service.ts +784 -0
  448. package/src/index.ts +29 -0
  449. package/src/instructor/dto/create-instructor.dto.ts +43 -0
  450. package/src/instructor/dto/update-instructor.dto.ts +38 -0
  451. package/src/instructor/instructor.controller.ts +73 -0
  452. package/src/instructor/instructor.module.ts +12 -0
  453. package/src/instructor/instructor.service.ts +646 -0
  454. package/src/lms.module.ts +36 -4
  455. package/src/reports/reports.controller.ts +14 -0
  456. package/src/reports/reports.module.ts +12 -0
  457. package/src/reports/reports.service.ts +485 -0
  458. package/src/training/dto/create-training.dto.ts +81 -0
  459. package/src/training/dto/update-training.dto.ts +4 -0
  460. package/src/training/training.controller.ts +68 -0
  461. package/src/training/training.module.ts +12 -0
  462. package/src/training/training.service.ts +574 -0
@@ -0,0 +1,726 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable } from '@nestjs/common';
3
+
4
+ const DASHBOARD_COLORS = [
5
+ '#f97316',
6
+ '#3b82f6',
7
+ '#22c55e',
8
+ '#a855f7',
9
+ '#ec4899',
10
+ '#14b8a6',
11
+ ];
12
+
13
+ @Injectable()
14
+ export class LmsDashboardService {
15
+ constructor(private readonly prisma: PrismaService) {}
16
+
17
+ async getDashboard(locale?: string) {
18
+ const localeCode = this.normalizeLocale(locale);
19
+ const now = new Date();
20
+ const calendarStart = this.startOfDay(this.addDays(now, -14));
21
+ const calendarEnd = this.endOfDay(this.addDays(now, 30));
22
+
23
+ const [
24
+ kpis,
25
+ studentGrowth,
26
+ engagement,
27
+ topCourses,
28
+ categoryDistribution,
29
+ calendarEvents,
30
+ latestEnrollments,
31
+ upcomingClasses,
32
+ ] = await Promise.all([
33
+ this.getKpis(now),
34
+ this.getStudentGrowth(localeCode, now),
35
+ this.getEngagement(localeCode, now),
36
+ this.getTopCourses(),
37
+ this.getCategoryDistribution(localeCode),
38
+ this.getCalendarEvents(calendarStart, calendarEnd),
39
+ this.getLatestEnrollments(),
40
+ this.getUpcomingClasses(now),
41
+ ]);
42
+
43
+ return {
44
+ kpis,
45
+ charts: {
46
+ studentGrowth,
47
+ engagement,
48
+ topCourses,
49
+ categoryDistribution,
50
+ },
51
+ calendarEvents,
52
+ latestEnrollments,
53
+ upcomingClasses,
54
+ footerSummary: {
55
+ activeClasses: kpis.activeClasses.value,
56
+ publishedCourses: kpis.activeCourses.value,
57
+ enrolledStudents: kpis.totalStudents.value,
58
+ certificates: kpis.issuedCertificates.value,
59
+ },
60
+ };
61
+ }
62
+
63
+ private async getKpis(now: Date) {
64
+ const currentMonthStart = this.startOfMonth(now);
65
+ const previousMonthStart = this.startOfMonth(this.addMonths(now, -1));
66
+ const previousMonthEnd = this.endOfMonth(this.addMonths(now, -1));
67
+
68
+ const [
69
+ totalStudents,
70
+ previousTotalStudents,
71
+ activeCourses,
72
+ previousActiveCourses,
73
+ activeClasses,
74
+ previousActiveClasses,
75
+ issuedCertificates,
76
+ previousIssuedCertificates,
77
+ overallCompletion,
78
+ currentCompletion,
79
+ previousCompletion,
80
+ overallAttempts,
81
+ currentAttempts,
82
+ previousAttempts,
83
+ ] = await Promise.all([
84
+ this.prisma.course_enrollment.count(),
85
+ this.prisma.course_enrollment.count({
86
+ where: { enrolled_at: { lt: currentMonthStart } },
87
+ }),
88
+ this.prisma.course.count({ where: { status: 'published' } }),
89
+ this.prisma.course.count({
90
+ where: {
91
+ status: 'published',
92
+ created_at: { lt: currentMonthStart },
93
+ },
94
+ }),
95
+ this.prisma.course_class_group.count({ where: { status: 'ongoing' } }),
96
+ this.prisma.course_class_group.count({
97
+ where: {
98
+ status: 'ongoing',
99
+ created_at: { lt: currentMonthStart },
100
+ },
101
+ }),
102
+ this.prisma.certificate.count(),
103
+ this.prisma.certificate.count({
104
+ where: { issued_at: { lt: currentMonthStart } },
105
+ }),
106
+ this.prisma.course_enrollment.aggregate({
107
+ where: { status: { in: ['active', 'completed'] } },
108
+ _avg: { progress_percent: true },
109
+ }),
110
+ this.prisma.course_enrollment.aggregate({
111
+ where: {
112
+ status: { in: ['active', 'completed'] },
113
+ updated_at: { gte: currentMonthStart },
114
+ },
115
+ _avg: { progress_percent: true },
116
+ }),
117
+ this.prisma.course_enrollment.aggregate({
118
+ where: {
119
+ status: { in: ['active', 'completed'] },
120
+ updated_at: {
121
+ gte: previousMonthStart,
122
+ lte: previousMonthEnd,
123
+ },
124
+ },
125
+ _avg: { progress_percent: true },
126
+ }),
127
+ this.prisma.exam_attempt.findMany({
128
+ where: {
129
+ status: 'completed',
130
+ score: { not: null },
131
+ },
132
+ select: {
133
+ score: true,
134
+ exam: { select: { min_score: true } },
135
+ },
136
+ }),
137
+ this.prisma.exam_attempt.findMany({
138
+ where: {
139
+ status: 'completed',
140
+ score: { not: null },
141
+ finished_at: { gte: currentMonthStart },
142
+ },
143
+ select: {
144
+ score: true,
145
+ exam: { select: { min_score: true } },
146
+ },
147
+ }),
148
+ this.prisma.exam_attempt.findMany({
149
+ where: {
150
+ status: 'completed',
151
+ score: { not: null },
152
+ finished_at: {
153
+ gte: previousMonthStart,
154
+ lte: previousMonthEnd,
155
+ },
156
+ },
157
+ select: {
158
+ score: true,
159
+ exam: { select: { min_score: true } },
160
+ },
161
+ }),
162
+ ]);
163
+
164
+ const overallApproval = this.calculateApprovalRate(overallAttempts);
165
+ const currentApproval = this.calculateApprovalRate(currentAttempts);
166
+ const previousApproval = this.calculateApprovalRate(previousAttempts);
167
+
168
+ return {
169
+ totalStudents: {
170
+ value: totalStudents,
171
+ change: this.percentChange(totalStudents, previousTotalStudents),
172
+ changeType: 'percent',
173
+ },
174
+ activeCourses: {
175
+ value: activeCourses,
176
+ change: activeCourses - previousActiveCourses,
177
+ changeType: 'absolute',
178
+ },
179
+ activeClasses: {
180
+ value: activeClasses,
181
+ change: activeClasses - previousActiveClasses,
182
+ changeType: 'absolute',
183
+ },
184
+ issuedCertificates: {
185
+ value: issuedCertificates,
186
+ change: issuedCertificates - previousIssuedCertificates,
187
+ changeType: 'absolute',
188
+ },
189
+ completionRate: {
190
+ value: Math.round(overallCompletion._avg.progress_percent ?? 0),
191
+ change: this.round((currentCompletion._avg.progress_percent ?? 0) - (previousCompletion._avg.progress_percent ?? 0), 1),
192
+ changeType: 'percent',
193
+ },
194
+ approvalRate: {
195
+ value: overallApproval,
196
+ change: this.round(currentApproval - previousApproval, 1),
197
+ changeType: 'percent',
198
+ },
199
+ };
200
+ }
201
+
202
+ private async getStudentGrowth(locale: string, now: Date) {
203
+ const formatter = new Intl.DateTimeFormat(locale, { month: 'short' });
204
+ const months = Array.from({ length: 7 }, (_, index) => {
205
+ const date = this.addMonths(now, index - 6);
206
+ return this.startOfMonth(date);
207
+ });
208
+
209
+ const counts = await Promise.all(
210
+ months.map((monthStart) =>
211
+ this.prisma.course_enrollment.count({
212
+ where: {
213
+ enrolled_at: { lte: this.endOfMonth(monthStart) },
214
+ },
215
+ }),
216
+ ),
217
+ );
218
+
219
+ return months.map((monthStart, index) => ({
220
+ label: this.capitalize(formatter.format(monthStart).replace('.', '')),
221
+ students: counts[index],
222
+ }));
223
+ }
224
+
225
+ private async getEngagement(locale: string, now: Date) {
226
+ const windowStart = this.startOfWeek(this.addDays(now, -7 * 7));
227
+ const [lessonProgress, examAttempts] = await Promise.all([
228
+ this.prisma.course_lesson_progress.findMany({
229
+ where: { updated_at: { gte: windowStart } },
230
+ select: {
231
+ updated_at: true,
232
+ course_lesson: { select: { type: true } },
233
+ },
234
+ }),
235
+ this.prisma.exam_attempt.findMany({
236
+ where: { started_at: { gte: windowStart } },
237
+ select: { started_at: true },
238
+ }),
239
+ ]);
240
+
241
+ const prefix = locale.startsWith('pt') ? 'S' : 'W';
242
+ const buckets = Array.from({ length: 8 }, (_, index) => {
243
+ const weekStart = this.startOfWeek(this.addDays(windowStart, index * 7));
244
+ const weekEnd = this.endOfDay(this.addDays(weekStart, 6));
245
+
246
+ const accessCount = lessonProgress.filter(
247
+ (item) => item.updated_at >= weekStart && item.updated_at <= weekEnd,
248
+ ).length;
249
+ const videoCount = lessonProgress.filter(
250
+ (item) =>
251
+ item.updated_at >= weekStart &&
252
+ item.updated_at <= weekEnd &&
253
+ item.course_lesson?.type === 'video',
254
+ ).length;
255
+ const exerciseCount = examAttempts.filter(
256
+ (item) => item.started_at >= weekStart && item.started_at <= weekEnd,
257
+ ).length;
258
+
259
+ return {
260
+ label: `${prefix}${index + 1}`,
261
+ accesses: accessCount,
262
+ videos: videoCount,
263
+ exercises: exerciseCount,
264
+ };
265
+ });
266
+
267
+ return buckets;
268
+ }
269
+
270
+ private async getTopCourses(limit = 6) {
271
+ const since = this.addDays(new Date(), -90);
272
+ const rows = await this.prisma.course_lesson_progress.findMany({
273
+ where: { updated_at: { gte: since } },
274
+ select: {
275
+ course_lesson: {
276
+ select: {
277
+ course_module: {
278
+ select: {
279
+ course: {
280
+ select: {
281
+ id: true,
282
+ title: true,
283
+ },
284
+ },
285
+ },
286
+ },
287
+ },
288
+ },
289
+ },
290
+ });
291
+
292
+ const counts = new Map<number, { title: string; accesses: number }>();
293
+
294
+ for (const row of rows) {
295
+ const course = row.course_lesson?.course_module?.course;
296
+ if (!course) {
297
+ continue;
298
+ }
299
+
300
+ const current = counts.get(course.id) ?? { title: course.title, accesses: 0 };
301
+ current.accesses += 1;
302
+ counts.set(course.id, current);
303
+ }
304
+
305
+ if (counts.size === 0) {
306
+ const grouped = await this.prisma.course_enrollment.groupBy({
307
+ by: ['course_id'],
308
+ _count: { course_id: true },
309
+ orderBy: { _count: { course_id: 'desc' } },
310
+ take: limit,
311
+ });
312
+ const courses = await this.prisma.course.findMany({
313
+ where: { id: { in: grouped.map((item) => item.course_id) } },
314
+ select: { id: true, title: true },
315
+ });
316
+ const titleById = new Map(courses.map((item) => [item.id, item.title]));
317
+
318
+ return grouped.map((item) => ({
319
+ title: titleById.get(item.course_id) ?? `#${item.course_id}`,
320
+ accesses: item._count.course_id,
321
+ }));
322
+ }
323
+
324
+ return Array.from(counts.values())
325
+ .sort((left, right) => right.accesses - left.accesses)
326
+ .slice(0, limit);
327
+ }
328
+
329
+ private async getCategoryDistribution(locale: string) {
330
+ const rows = await this.prisma.course_category.findMany({
331
+ where: { course: { status: 'published' } },
332
+ select: {
333
+ course_id: true,
334
+ category: {
335
+ select: {
336
+ slug: true,
337
+ category_locale: {
338
+ where: { locale: { code: locale.slice(0, 2) as 'en' | 'pt' } },
339
+ take: 1,
340
+ select: { name: true },
341
+ },
342
+ },
343
+ },
344
+ },
345
+ });
346
+
347
+ const courseIds = new Set(rows.map((item) => item.course_id));
348
+ const counts = new Map<string, { name: string; count: number }>();
349
+
350
+ for (const row of rows) {
351
+ const key = row.category?.slug ?? 'uncategorized';
352
+ const current =
353
+ counts.get(key) ?? {
354
+ name: row.category?.category_locale?.[0]?.name ?? row.category?.slug ?? key,
355
+ count: 0,
356
+ };
357
+ current.count += 1;
358
+ counts.set(key, current);
359
+ }
360
+
361
+ const total = Math.max(courseIds.size, 1);
362
+
363
+ return Array.from(counts.values())
364
+ .map((item) => ({
365
+ name: item.name,
366
+ value: this.round((item.count / total) * 100, 0),
367
+ }))
368
+ .sort((left, right) => right.value - left.value)
369
+ .slice(0, 6);
370
+ }
371
+
372
+ private async getCalendarEvents(start: Date, end: Date) {
373
+ const sessions = await this.prisma.course_class_session.findMany({
374
+ where: {
375
+ session_date: {
376
+ gte: start,
377
+ lte: end,
378
+ },
379
+ },
380
+ orderBy: [{ session_date: 'asc' }, { start_time: 'asc' }],
381
+ include: {
382
+ course_class_group: {
383
+ select: {
384
+ id: true,
385
+ title: true,
386
+ delivery_mode: true,
387
+ status: true,
388
+ capacity: true,
389
+ location: true,
390
+ virtual_room_url: true,
391
+ course: {
392
+ select: {
393
+ id: true,
394
+ title: true,
395
+ primary_color: true,
396
+ },
397
+ },
398
+ _count: {
399
+ select: {
400
+ course_enrollment: true,
401
+ },
402
+ },
403
+ },
404
+ },
405
+ course_class_session_instructor: {
406
+ select: {
407
+ role: true,
408
+ instructor: {
409
+ select: {
410
+ person: {
411
+ select: {
412
+ name: true,
413
+ },
414
+ },
415
+ },
416
+ },
417
+ },
418
+ },
419
+ },
420
+ });
421
+
422
+ return sessions.map((session) => {
423
+ const classGroup = session.course_class_group;
424
+ const course = classGroup?.course;
425
+ const instructors = this.formatInstructorNames(
426
+ session.course_class_session_instructor,
427
+ );
428
+ const color =
429
+ course?.primary_color ??
430
+ DASHBOARD_COLORS[(course?.id ?? session.id) % DASHBOARD_COLORS.length];
431
+ const startDate = this.combineDateAndTime(
432
+ session.session_date,
433
+ session.start_time,
434
+ );
435
+ const endDate = this.combineDateAndTime(session.session_date, session.end_time);
436
+
437
+ return {
438
+ id: session.id,
439
+ title: session.title ?? classGroup?.title,
440
+ start: this.formatLocalISOString(startDate),
441
+ end: this.formatLocalISOString(endDate),
442
+ resource: {
443
+ id: String(classGroup?.id ?? session.id),
444
+ cor: color,
445
+ curso: course?.title ?? '',
446
+ professor: instructors,
447
+ tipo: this.normalizeDeliveryMode(classGroup?.delivery_mode),
448
+ local:
449
+ session.meeting_url ??
450
+ session.location ??
451
+ classGroup?.virtual_room_url ??
452
+ classGroup?.location ??
453
+ '',
454
+ vagas: classGroup?.capacity ?? 0,
455
+ matriculados: classGroup?._count?.course_enrollment ?? 0,
456
+ status: this.normalizeClassStatus(classGroup?.status),
457
+ turmaName: classGroup?.title ?? session.title,
458
+ },
459
+ };
460
+ });
461
+ }
462
+
463
+ private async getLatestEnrollments(limit = 12) {
464
+ const enrollments = await this.prisma.course_enrollment.findMany({
465
+ take: limit,
466
+ orderBy: { enrolled_at: 'desc' },
467
+ include: {
468
+ course: {
469
+ select: {
470
+ title: true,
471
+ },
472
+ },
473
+ person: {
474
+ select: {
475
+ name: true,
476
+ contact: {
477
+ orderBy: { is_primary: 'desc' },
478
+ select: {
479
+ value: true,
480
+ is_primary: true,
481
+ },
482
+ },
483
+ },
484
+ },
485
+ },
486
+ });
487
+
488
+ return enrollments.map((enrollment) => ({
489
+ id: enrollment.id,
490
+ studentName: enrollment.person?.name ?? '',
491
+ email: this.extractEmail(enrollment.person?.contact ?? []),
492
+ courseTitle: enrollment.course?.title ?? '',
493
+ enrolledAt: enrollment.enrolled_at.toISOString(),
494
+ status: this.normalizeEnrollmentStatus(enrollment.status),
495
+ }));
496
+ }
497
+
498
+ private async getUpcomingClasses(now: Date, limit = 10) {
499
+ const sessions = await this.prisma.course_class_session.findMany({
500
+ take: limit,
501
+ where: {
502
+ session_date: { gte: this.startOfDay(now) },
503
+ status: { not: 'cancelled' },
504
+ },
505
+ orderBy: [{ session_date: 'asc' }, { start_time: 'asc' }],
506
+ include: {
507
+ course_class_group: {
508
+ select: {
509
+ id: true,
510
+ title: true,
511
+ },
512
+ },
513
+ course_class_session_instructor: {
514
+ select: {
515
+ role: true,
516
+ instructor: {
517
+ select: {
518
+ person: {
519
+ select: {
520
+ name: true,
521
+ },
522
+ },
523
+ },
524
+ },
525
+ },
526
+ },
527
+ },
528
+ });
529
+
530
+ return sessions.map((session) => ({
531
+ id: session.id,
532
+ className: session.course_class_group?.title ?? session.title,
533
+ subject: session.title,
534
+ teacher: this.formatInstructorNames(session.course_class_session_instructor),
535
+ date: this.formatLocalISOString(this.combineDateAndTime(session.session_date, null)),
536
+ startTime: session.start_time,
537
+ endTime: session.end_time,
538
+ }));
539
+ }
540
+
541
+ private calculateApprovalRate(
542
+ attempts: Array<{ score: number | null; exam: { min_score: number | null } }>,
543
+ ) {
544
+ if (attempts.length === 0) {
545
+ return 0;
546
+ }
547
+
548
+ const approved = attempts.filter(
549
+ (attempt) => (attempt.score ?? 0) >= (attempt.exam?.min_score ?? 0),
550
+ ).length;
551
+
552
+ return this.round((approved / attempts.length) * 100, 1);
553
+ }
554
+
555
+ private normalizeLocale(locale?: string) {
556
+ if (!locale) {
557
+ return 'pt-BR';
558
+ }
559
+
560
+ return locale.toLowerCase().startsWith('en') ? 'en-US' : 'pt-BR';
561
+ }
562
+
563
+ private normalizeDeliveryMode(value?: string | null) {
564
+ if (value === 'presential') {
565
+ return 'inPerson';
566
+ }
567
+
568
+ if (value === 'hybrid') {
569
+ return 'hybrid';
570
+ }
571
+
572
+ return 'online';
573
+ }
574
+
575
+ private normalizeClassStatus(value?: string | null) {
576
+ if (value === 'ongoing') {
577
+ return 'inProgress';
578
+ }
579
+
580
+ if (value === 'completed') {
581
+ return 'completed';
582
+ }
583
+
584
+ if (value === 'cancelled') {
585
+ return 'cancelled';
586
+ }
587
+
588
+ return 'open';
589
+ }
590
+
591
+ private normalizeEnrollmentStatus(value?: string | null) {
592
+ if (value === 'pending') {
593
+ return 'pending';
594
+ }
595
+
596
+ if (value === 'completed') {
597
+ return 'completed';
598
+ }
599
+
600
+ if (value === 'cancelled') {
601
+ return 'cancelled';
602
+ }
603
+
604
+ if (value === 'paused') {
605
+ return 'paused';
606
+ }
607
+
608
+ return 'active';
609
+ }
610
+
611
+ private extractEmail(contacts: Array<{ value: string; is_primary: boolean }>) {
612
+ const sorted = [...contacts].sort(
613
+ (left, right) => Number(right.is_primary) - Number(left.is_primary),
614
+ );
615
+
616
+ return (
617
+ sorted.find((contact) => contact.value.includes('@'))?.value ??
618
+ sorted[0]?.value ??
619
+ ''
620
+ );
621
+ }
622
+
623
+ private formatInstructorNames(
624
+ instructors: Array<{
625
+ role?: string;
626
+ instructor?: { person?: { name?: string | null } | null } | null;
627
+ }>,
628
+ ) {
629
+ const names = instructors
630
+ .sort((left, right) => {
631
+ if (left.role === 'lead') return -1;
632
+ if (right.role === 'lead') return 1;
633
+ return 0;
634
+ })
635
+ .map((item) => item.instructor?.person?.name)
636
+ .filter((value): value is string => Boolean(value));
637
+
638
+ return names.length > 0 ? names.join(', ') : '-';
639
+ }
640
+
641
+ private percentChange(current: number, previous: number) {
642
+ if (previous <= 0) {
643
+ return current > 0 ? 100 : 0;
644
+ }
645
+
646
+ return this.round(((current - previous) / previous) * 100, 1);
647
+ }
648
+
649
+ private round(value: number, digits = 0) {
650
+ const factor = 10 ** digits;
651
+ return Math.round(value * factor) / factor;
652
+ }
653
+
654
+ private addDays(date: Date, days: number) {
655
+ const result = new Date(date);
656
+ result.setDate(result.getDate() + days);
657
+ return result;
658
+ }
659
+
660
+ private addMonths(date: Date, months: number) {
661
+ const result = new Date(date);
662
+ result.setMonth(result.getMonth() + months);
663
+ return result;
664
+ }
665
+
666
+ private startOfDay(date: Date) {
667
+ const result = new Date(date);
668
+ result.setHours(0, 0, 0, 0);
669
+ return result;
670
+ }
671
+
672
+ private endOfDay(date: Date) {
673
+ const result = new Date(date);
674
+ result.setHours(23, 59, 59, 999);
675
+ return result;
676
+ }
677
+
678
+ private startOfMonth(date: Date) {
679
+ return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
680
+ }
681
+
682
+ private endOfMonth(date: Date) {
683
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
684
+ }
685
+
686
+ private startOfWeek(date: Date) {
687
+ const result = this.startOfDay(date);
688
+ const currentDay = result.getDay();
689
+ const diff = currentDay === 0 ? -6 : 1 - currentDay;
690
+ result.setDate(result.getDate() + diff);
691
+ return result;
692
+ }
693
+
694
+ private combineDateAndTime(date: Date, time?: string | null) {
695
+ // Prisma returns date-only fields as UTC midnight; extract parts via UTC
696
+ // methods so the calendar date is never shifted by the server timezone.
697
+ const year = date.getUTCFullYear();
698
+ const month = date.getUTCMonth();
699
+ const day = date.getUTCDate();
700
+
701
+ let hours = 0;
702
+ let minutes = 0;
703
+
704
+ if (time) {
705
+ const parts = time.split(':');
706
+ hours = Number(parts[0]) || 0;
707
+ minutes = Number(parts[1]) || 0;
708
+ }
709
+
710
+ return new Date(year, month, day, hours, minutes, 0, 0);
711
+ }
712
+
713
+ /** Serialise a Date as a local ISO string with no timezone suffix so the
714
+ * browser treats it as local time instead of converting from UTC. */
715
+ private formatLocalISOString(date: Date): string {
716
+ const pad = (n: number) => String(n).padStart(2, '0');
717
+ return (
718
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
719
+ `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
720
+ );
721
+ }
722
+
723
+ private capitalize(value: string) {
724
+ return value.charAt(0).toUpperCase() + value.slice(1);
725
+ }
726
+ }