@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
@@ -0,0 +1,916 @@
1
+ 'use client';
2
+
3
+ import { useSidebar } from '@/components/ui/sidebar';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import {
6
+ getCanvasAPI,
7
+ registerCanvasAPI,
8
+ unregisterCanvasAPI,
9
+ type CanvasAPI,
10
+ } from '../../_lib/editor/canvasInstance';
11
+ import {
12
+ createFabricFromTemplate,
13
+ fabricObjToFull,
14
+ fabricObjToTemplatePartial,
15
+ templateJSONToFabric,
16
+ } from '../../_lib/editor/templateSerializer';
17
+ import type { TemplateObject } from '../../_lib/editor/types';
18
+ import { CANVAS_H, CANVAS_W } from '../../_lib/editor/types';
19
+ import { useTemplateStore } from '../../_lib/store/useTemplateStore';
20
+
21
+ /* ── snapping constants ── */
22
+ const SNAP_TOLERANCE = 6; // px in canvas space
23
+
24
+ /* guide colours */
25
+ const GUIDE_COLOR = '#3b82f6'; // blue-500
26
+ const GRID_COLOR = '#e2e8f0'; // slate-200
27
+ const MARGIN_COLOR = '#f97316'; // orange-500
28
+ const MARGIN_INSET = 60; // px from canvas edge
29
+ const MIN_ZOOM = 0.2;
30
+ const MAX_ZOOM = 3.0;
31
+ const FIT_PADDING = 24;
32
+
33
+ function applyCanvasZoom(canvas: any, appliedZoom: number) {
34
+ canvas.setZoom(appliedZoom);
35
+ canvas.setDimensions({
36
+ width: CANVAS_W * appliedZoom,
37
+ height: CANVAS_H * appliedZoom,
38
+ });
39
+ canvas.requestRenderAll();
40
+ }
41
+
42
+ type CanvasStageProps = {
43
+ isLoading?: boolean;
44
+ };
45
+
46
+ export default function CanvasStage({ isLoading = false }: CanvasStageProps) {
47
+ const { state: sidebarState } = useSidebar();
48
+ const canvasElRef = useRef<HTMLCanvasElement>(null);
49
+ const wrapperRef = useRef<HTMLDivElement>(null);
50
+ /* eslint-disable @typescript-eslint/no-explicit-any */
51
+ const fabricRef = useRef<any>(null);
52
+ const fabricModRef = useRef<any>(null);
53
+ const initDone = useRef(false);
54
+ const isPanning = useRef(false);
55
+ const lastPanPoint = useRef<{ x: number; y: number } | null>(null);
56
+ const spaceHeld = useRef(false);
57
+ /** Refs to guide line objects currently on canvas */
58
+ const guideLines = useRef<any[]>([]);
59
+ /** Refs to grid/margin objects */
60
+ const gridObjects = useRef<any[]>([]);
61
+ const marginRect = useRef<any>(null);
62
+
63
+ const [isDragOver, setIsDragOver] = useState(false);
64
+ const [autoFitZoom, setAutoFitZoom] = useState(0.5);
65
+
66
+ const zoom = useTemplateStore((s) => s.zoom);
67
+ const setEffectiveZoom = useTemplateStore((s) => s.setEffectiveZoom);
68
+ const snapEnabled = useTemplateStore((s) => s.snapEnabled);
69
+ const gridEnabled = useTemplateStore((s) => s.gridEnabled);
70
+ const marginsEnabled = useTemplateStore((s) => s.marginsEnabled);
71
+
72
+ /* keep refs current for use inside Fabric callbacks */
73
+ const snapRef = useRef(snapEnabled);
74
+ const autoFitZoomRef = useRef(autoFitZoom);
75
+ snapRef.current = snapEnabled;
76
+ autoFitZoomRef.current = autoFitZoom;
77
+
78
+ function clampZoom(value: number) {
79
+ return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value));
80
+ }
81
+
82
+ /* ── initialise fabric (once) ── */
83
+ useEffect(() => {
84
+ if (initDone.current) return;
85
+ initDone.current = true;
86
+
87
+ let canvas: any;
88
+
89
+ async function init() {
90
+ const fabricMod = await import('fabric');
91
+ fabricModRef.current = fabricMod;
92
+ const { Canvas } = fabricMod;
93
+
94
+ const el = canvasElRef.current;
95
+ if (!el) return;
96
+
97
+ const { zoom: manualZoom, effectiveZoom } = useTemplateStore.getState();
98
+ const z = Math.min(manualZoom, effectiveZoom);
99
+
100
+ canvas = new Canvas(el, {
101
+ width: CANVAS_W * z,
102
+ height: CANVAS_H * z,
103
+ backgroundColor: '#ffffff',
104
+ selection: true,
105
+ preserveObjectStacking: true,
106
+ });
107
+ canvas.setZoom(z);
108
+ fabricRef.current = canvas;
109
+
110
+ /* load existing template objects */
111
+ const template = useTemplateStore.getState().template;
112
+ templateJSONToFabric(canvas, template, fabricMod);
113
+
114
+ if (template.background.src) {
115
+ await applyBg(canvas, fabricMod, template.background.src);
116
+ }
117
+
118
+ /* ── fabric events ── */
119
+ canvas.on('selection:created', onSelect);
120
+ canvas.on('selection:updated', onSelect);
121
+ canvas.on('selection:cleared', () =>
122
+ useTemplateStore.getState().selectObject(null)
123
+ );
124
+ canvas.on('object:modified', (e: any) => {
125
+ clearGuides(canvas);
126
+ onModified(e);
127
+ });
128
+ canvas.on('object:moving', (e: any) => {
129
+ if (snapRef.current) applySnapping(canvas, e.target);
130
+ onModified(e);
131
+ });
132
+ canvas.on('object:scaling', onModified);
133
+ canvas.on('object:rotating', onModified);
134
+ canvas.on('text:changed', onModified);
135
+
136
+ /* ── ctrl+wheel zoom ── */
137
+ canvas.on('mouse:wheel', (opt: any) => {
138
+ const e: WheelEvent = opt.e;
139
+ if (!e.ctrlKey && !e.metaKey) return;
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+
143
+ const delta = -e.deltaY;
144
+ const currentManualZoom = useTemplateStore.getState().zoom;
145
+ const newManualZoom = clampZoom(currentManualZoom * (1 + delta / 500));
146
+ const appliedZoom = Math.min(newManualZoom, autoFitZoomRef.current);
147
+
148
+ const point =
149
+ typeof canvas.getScenePoint === 'function'
150
+ ? canvas.getScenePoint(e)
151
+ : typeof canvas.getViewportPoint === 'function'
152
+ ? canvas.getViewportPoint(e)
153
+ : canvas.getPointer(e, true);
154
+ canvas.zoomToPoint(point, appliedZoom);
155
+ canvas.setDimensions({
156
+ width: CANVAS_W * appliedZoom,
157
+ height: CANVAS_H * appliedZoom,
158
+ });
159
+ canvas.requestRenderAll();
160
+ useTemplateStore.getState().setZoom(newManualZoom);
161
+ useTemplateStore.getState().setEffectiveZoom(appliedZoom);
162
+ });
163
+
164
+ /* ── mouse pan (middle-click or space+drag) ── */
165
+ canvas.on('mouse:down', (opt: any) => {
166
+ const e: MouseEvent = opt.e;
167
+ if (e.button === 1 || spaceHeld.current) {
168
+ isPanning.current = true;
169
+ lastPanPoint.current = { x: e.clientX, y: e.clientY };
170
+ canvas.selection = false;
171
+ canvas.defaultCursor = 'grabbing';
172
+ e.preventDefault();
173
+ }
174
+ });
175
+
176
+ canvas.on('mouse:move', (opt: any) => {
177
+ if (!isPanning.current || !lastPanPoint.current) return;
178
+ const e: MouseEvent = opt.e;
179
+ const vpt = canvas.viewportTransform.slice();
180
+ vpt[4] += e.clientX - lastPanPoint.current.x;
181
+ vpt[5] += e.clientY - lastPanPoint.current.y;
182
+ canvas.setViewportTransform(vpt);
183
+ lastPanPoint.current = { x: e.clientX, y: e.clientY };
184
+ });
185
+
186
+ canvas.on('mouse:up', () => {
187
+ if (isPanning.current) {
188
+ isPanning.current = false;
189
+ lastPanPoint.current = null;
190
+ canvas.selection = true;
191
+ canvas.defaultCursor = 'default';
192
+ }
193
+ /* clear guides on mouse up */
194
+ clearGuides(canvas);
195
+ });
196
+
197
+ /* ── register API for other panels ── */
198
+ registerCanvasAPI(buildAPI(canvas, fabricMod));
199
+ }
200
+
201
+ init();
202
+
203
+ return () => {
204
+ unregisterCanvasAPI();
205
+ canvas?.dispose();
206
+ };
207
+ // eslint-disable-next-line react-hooks/exhaustive-deps
208
+ }, []);
209
+
210
+ /* ── measure available stage size and compute fit zoom ── */
211
+ useEffect(() => {
212
+ const wrapper = wrapperRef.current;
213
+ if (!wrapper) return;
214
+ const el: HTMLDivElement = wrapper;
215
+
216
+ function measureFitZoom() {
217
+ const styles = window.getComputedStyle(el);
218
+ const horizontalPadding =
219
+ parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);
220
+ const verticalPadding =
221
+ parseFloat(styles.paddingTop) + parseFloat(styles.paddingBottom);
222
+ const availableWidth = Math.max(
223
+ 1,
224
+ el.clientWidth - horizontalPadding - FIT_PADDING * 2
225
+ );
226
+ const availableHeight = Math.max(
227
+ 1,
228
+ el.clientHeight - verticalPadding - FIT_PADDING * 2
229
+ );
230
+
231
+ const fitZoom = clampZoom(
232
+ Math.min(availableWidth / CANVAS_W, availableHeight / CANVAS_H)
233
+ );
234
+ setAutoFitZoom((current) =>
235
+ Math.abs(current - fitZoom) < 0.001 ? current : fitZoom
236
+ );
237
+ }
238
+
239
+ measureFitZoom();
240
+
241
+ const observer = new ResizeObserver(() => {
242
+ measureFitZoom();
243
+ });
244
+ observer.observe(el);
245
+
246
+ const transitionTimer = window.setTimeout(measureFitZoom, 220);
247
+
248
+ return () => {
249
+ window.clearTimeout(transitionTimer);
250
+ observer.disconnect();
251
+ };
252
+ }, [sidebarState]);
253
+
254
+ /* ── sync effective zoom to fit + manual zoom ── */
255
+ useEffect(() => {
256
+ const c = fabricRef.current;
257
+ const appliedZoom = Math.min(zoom, autoFitZoom);
258
+
259
+ setEffectiveZoom(appliedZoom);
260
+
261
+ if (!c) return;
262
+ const current = c.getZoom();
263
+ if (Math.abs(current - appliedZoom) < 0.001) return;
264
+ applyCanvasZoom(c, appliedZoom);
265
+ }, [autoFitZoom, setEffectiveZoom, zoom]);
266
+
267
+ /* ── grid overlay ── */
268
+ useEffect(() => {
269
+ const c = fabricRef.current;
270
+ const fm = fabricModRef.current;
271
+ if (!c || !fm) return;
272
+
273
+ // remove existing grid objects
274
+ gridObjects.current.forEach((o) => c.remove(o));
275
+ gridObjects.current = [];
276
+
277
+ if (gridEnabled) {
278
+ const step = 50; // px grid step
279
+ const lines: any[] = [];
280
+
281
+ // vertical lines
282
+ for (let x = step; x < CANVAS_W; x += step) {
283
+ const line = new fm.Line([x, 0, x, CANVAS_H], {
284
+ stroke: GRID_COLOR,
285
+ strokeWidth: x % 200 === 0 ? 0.8 : 0.4,
286
+ selectable: false,
287
+ evented: false,
288
+ excludeFromExport: true,
289
+ });
290
+ line._isGuide = true;
291
+ lines.push(line);
292
+ }
293
+ // horizontal lines
294
+ for (let y = step; y < CANVAS_H; y += step) {
295
+ const line = new fm.Line([0, y, CANVAS_W, y], {
296
+ stroke: GRID_COLOR,
297
+ strokeWidth: y % 200 === 0 ? 0.8 : 0.4,
298
+ selectable: false,
299
+ evented: false,
300
+ excludeFromExport: true,
301
+ });
302
+ line._isGuide = true;
303
+ lines.push(line);
304
+ }
305
+
306
+ lines.forEach((l) => c.add(l));
307
+ // send grid to back
308
+ lines.forEach((l) => c.sendObjectToBack(l));
309
+ gridObjects.current = lines;
310
+ c.requestRenderAll();
311
+ }
312
+ }, [gridEnabled]);
313
+
314
+ /* ── safe margins overlay ── */
315
+ useEffect(() => {
316
+ const c = fabricRef.current;
317
+ const fm = fabricModRef.current;
318
+ if (!c || !fm) return;
319
+
320
+ // remove existing margin rect
321
+ if (marginRect.current) {
322
+ c.remove(marginRect.current);
323
+ marginRect.current = null;
324
+ }
325
+
326
+ if (marginsEnabled) {
327
+ const rect = new fm.Rect({
328
+ left: MARGIN_INSET,
329
+ top: MARGIN_INSET,
330
+ width: CANVAS_W - MARGIN_INSET * 2,
331
+ height: CANVAS_H - MARGIN_INSET * 2,
332
+ fill: 'transparent',
333
+ stroke: MARGIN_COLOR,
334
+ strokeWidth: 1,
335
+ strokeDashArray: [8, 4],
336
+ selectable: false,
337
+ evented: false,
338
+ excludeFromExport: true,
339
+ });
340
+ rect._isGuide = true;
341
+ c.add(rect);
342
+ // bring to front (above grid, but guides are transparent)
343
+ c.bringObjectToFront(rect);
344
+ marginRect.current = rect;
345
+ c.requestRenderAll();
346
+ }
347
+ }, [marginsEnabled]);
348
+
349
+ /* ── keyboard events ── */
350
+ useEffect(() => {
351
+ function onKeyDown(e: KeyboardEvent) {
352
+ if (e.code === 'Space' && !e.repeat) {
353
+ const tag = (e.target as HTMLElement)?.tagName;
354
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
355
+ spaceHeld.current = true;
356
+ if (fabricRef.current) fabricRef.current.defaultCursor = 'grab';
357
+ e.preventDefault();
358
+ }
359
+
360
+ if (e.code === 'Delete' || e.code === 'Backspace') {
361
+ const tag = (e.target as HTMLElement)?.tagName;
362
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
363
+ const c = fabricRef.current;
364
+ if (c?.getActiveObject()?.isEditing) return;
365
+ const selId = useTemplateStore.getState().selectedObjectId;
366
+ if (selId) {
367
+ getCanvasAPI()?.removeObject(selId);
368
+ e.preventDefault();
369
+ }
370
+ }
371
+
372
+ if ((e.ctrlKey || e.metaKey) && e.code === 'KeyD') {
373
+ e.preventDefault();
374
+ const selId = useTemplateStore.getState().selectedObjectId;
375
+ if (selId) {
376
+ getCanvasAPI()?.duplicateObject(selId);
377
+ }
378
+ }
379
+ }
380
+
381
+ function onKeyUp(e: KeyboardEvent) {
382
+ if (e.code === 'Space') {
383
+ spaceHeld.current = false;
384
+ if (!isPanning.current) {
385
+ if (fabricRef.current) fabricRef.current.defaultCursor = 'default';
386
+ }
387
+ }
388
+ }
389
+
390
+ window.addEventListener('keydown', onKeyDown);
391
+ window.addEventListener('keyup', onKeyUp);
392
+ return () => {
393
+ window.removeEventListener('keydown', onKeyDown);
394
+ window.removeEventListener('keyup', onKeyUp);
395
+ };
396
+ }, []);
397
+
398
+ /* ── snapping helper ── */
399
+ function applySnapping(canvas: any, target: any) {
400
+ if (!target || target._isGuide) return;
401
+
402
+ const objLeft = target.left ?? 0;
403
+ const objTop = target.top ?? 0;
404
+ const w = (target.width ?? 0) * (target.scaleX ?? 1);
405
+ const h = (target.height ?? 0) * (target.scaleY ?? 1);
406
+ const objCX = objLeft + w / 2;
407
+ const objCY = objTop + h / 2;
408
+ const objRight = objLeft + w;
409
+ const objBottom = objTop + h;
410
+
411
+ // tolerance scales with zoom
412
+ const tol = SNAP_TOLERANCE / (canvas.getZoom() || 1);
413
+
414
+ // collect snap lines
415
+ const vSnaps: { pos: number; guideX: number }[] = [];
416
+ const hSnaps: { pos: number; guideY: number }[] = [];
417
+
418
+ // canvas center & edges
419
+ const canvasCX = CANVAS_W / 2;
420
+ const canvasCY = CANVAS_H / 2;
421
+
422
+ // Check canvas center X
423
+ if (Math.abs(objCX - canvasCX) < tol) {
424
+ vSnaps.push({ pos: canvasCX - w / 2, guideX: canvasCX });
425
+ }
426
+ // canvas left edge
427
+ if (Math.abs(objLeft) < tol) {
428
+ vSnaps.push({ pos: 0, guideX: 0 });
429
+ }
430
+ // canvas right edge
431
+ if (Math.abs(objRight - CANVAS_W) < tol) {
432
+ vSnaps.push({ pos: CANVAS_W - w, guideX: CANVAS_W });
433
+ }
434
+ // canvas center Y
435
+ if (Math.abs(objCY - canvasCY) < tol) {
436
+ hSnaps.push({ pos: canvasCY - h / 2, guideY: canvasCY });
437
+ }
438
+ // canvas top edge
439
+ if (Math.abs(objTop) < tol) {
440
+ hSnaps.push({ pos: 0, guideY: 0 });
441
+ }
442
+ // canvas bottom edge
443
+ if (Math.abs(objBottom - CANVAS_H) < tol) {
444
+ hSnaps.push({ pos: CANVAS_H - h, guideY: CANVAS_H });
445
+ }
446
+
447
+ // margins
448
+ if (useTemplateStore.getState().marginsEnabled) {
449
+ if (Math.abs(objLeft - MARGIN_INSET) < tol) {
450
+ vSnaps.push({ pos: MARGIN_INSET, guideX: MARGIN_INSET });
451
+ }
452
+ if (Math.abs(objRight - (CANVAS_W - MARGIN_INSET)) < tol) {
453
+ vSnaps.push({
454
+ pos: CANVAS_W - MARGIN_INSET - w,
455
+ guideX: CANVAS_W - MARGIN_INSET,
456
+ });
457
+ }
458
+ if (Math.abs(objTop - MARGIN_INSET) < tol) {
459
+ hSnaps.push({ pos: MARGIN_INSET, guideY: MARGIN_INSET });
460
+ }
461
+ if (Math.abs(objBottom - (CANVAS_H - MARGIN_INSET)) < tol) {
462
+ hSnaps.push({
463
+ pos: CANVAS_H - MARGIN_INSET - h,
464
+ guideY: CANVAS_H - MARGIN_INSET,
465
+ });
466
+ }
467
+ }
468
+
469
+ // snap to other objects (edges and center)
470
+ const others = canvas
471
+ .getObjects()
472
+ .filter((o: any) => o !== target && !o._isGuide);
473
+ for (const other of others) {
474
+ const oL = other.left ?? 0;
475
+ const oT = other.top ?? 0;
476
+ const oW = (other.width ?? 0) * (other.scaleX ?? 1);
477
+ const oH = (other.height ?? 0) * (other.scaleY ?? 1);
478
+ const oCX = oL + oW / 2;
479
+ const oCY = oT + oH / 2;
480
+ const oR = oL + oW;
481
+ const oB = oT + oH;
482
+
483
+ // vertical snaps (x-axis alignment)
484
+ if (Math.abs(objLeft - oL) < tol) vSnaps.push({ pos: oL, guideX: oL });
485
+ if (Math.abs(objRight - oR) < tol)
486
+ vSnaps.push({ pos: oR - w, guideX: oR });
487
+ if (Math.abs(objCX - oCX) < tol)
488
+ vSnaps.push({ pos: oCX - w / 2, guideX: oCX });
489
+ if (Math.abs(objLeft - oR) < tol) vSnaps.push({ pos: oR, guideX: oR });
490
+ if (Math.abs(objRight - oL) < tol)
491
+ vSnaps.push({ pos: oL - w, guideX: oL });
492
+
493
+ // horizontal snaps (y-axis alignment)
494
+ if (Math.abs(objTop - oT) < tol) hSnaps.push({ pos: oT, guideY: oT });
495
+ if (Math.abs(objBottom - oB) < tol)
496
+ hSnaps.push({ pos: oB - h, guideY: oB });
497
+ if (Math.abs(objCY - oCY) < tol)
498
+ hSnaps.push({ pos: oCY - h / 2, guideY: oCY });
499
+ if (Math.abs(objTop - oB) < tol) hSnaps.push({ pos: oB, guideY: oB });
500
+ if (Math.abs(objBottom - oT) < tol)
501
+ hSnaps.push({ pos: oT - h, guideY: oT });
502
+ }
503
+
504
+ // clear old guides
505
+ clearGuides(canvas);
506
+
507
+ // apply closest snap
508
+ if (vSnaps.length > 0) {
509
+ const best = vSnaps.reduce((a, b) =>
510
+ Math.abs(a.pos - objLeft) < Math.abs(b.pos - objLeft) ? a : b
511
+ );
512
+ target.set('left', best.pos);
513
+ addGuideLine(canvas, 'vertical', best.guideX);
514
+ }
515
+
516
+ if (hSnaps.length > 0) {
517
+ const best = hSnaps.reduce((a, b) =>
518
+ Math.abs(a.pos - objTop) < Math.abs(b.pos - objTop) ? a : b
519
+ );
520
+ target.set('top', best.pos);
521
+ addGuideLine(canvas, 'horizontal', best.guideY);
522
+ }
523
+ }
524
+
525
+ function addGuideLine(
526
+ canvas: any,
527
+ dir: 'vertical' | 'horizontal',
528
+ pos: number
529
+ ) {
530
+ const fm = fabricModRef.current;
531
+ if (!fm) return;
532
+
533
+ const coords =
534
+ dir === 'vertical' ? [pos, 0, pos, CANVAS_H] : [0, pos, CANVAS_W, pos];
535
+
536
+ const line = new fm.Line(coords, {
537
+ stroke: GUIDE_COLOR,
538
+ strokeWidth: 1,
539
+ strokeDashArray: [4, 3],
540
+ selectable: false,
541
+ evented: false,
542
+ excludeFromExport: true,
543
+ });
544
+ line._isGuide = true;
545
+ canvas.add(line);
546
+ canvas.bringObjectToFront(line);
547
+ guideLines.current.push(line);
548
+ }
549
+
550
+ function clearGuides(canvas: any) {
551
+ guideLines.current.forEach((l) => canvas.remove(l));
552
+ guideLines.current = [];
553
+ }
554
+
555
+ /* ── HTML5 drop target ── */
556
+ function handleDragOver(e: React.DragEvent) {
557
+ e.preventDefault();
558
+ e.dataTransfer.dropEffect = 'copy';
559
+ if (!isDragOver) setIsDragOver(true);
560
+ }
561
+
562
+ function handleDragLeave(e: React.DragEvent) {
563
+ if (!wrapperRef.current?.contains(e.relatedTarget as Node)) {
564
+ setIsDragOver(false);
565
+ }
566
+ }
567
+
568
+ function handleDrop(e: React.DragEvent) {
569
+ e.preventDefault();
570
+ setIsDragOver(false);
571
+
572
+ const raw = e.dataTransfer.getData('application/x-editor-element');
573
+ if (!raw) return;
574
+
575
+ try {
576
+ const data = JSON.parse(raw) as {
577
+ type: 'field' | 'staticText' | 'image' | 'shape';
578
+ key?: string;
579
+ shape?: 'rect' | 'line';
580
+ };
581
+
582
+ const api = getCanvasAPI();
583
+ if (!api) return;
584
+
585
+ const pt = api.clientToCanvas(e.clientX, e.clientY);
586
+ if (pt) {
587
+ api.addObjectAt(data.type, pt.x, pt.y, {
588
+ key: data.key,
589
+ shape: data.shape,
590
+ });
591
+ } else {
592
+ api.addObject(data.type, { key: data.key, shape: data.shape });
593
+ }
594
+ } catch {
595
+ /* ignore bad data */
596
+ }
597
+ }
598
+
599
+ return (
600
+ <div
601
+ ref={wrapperRef}
602
+ className={`relative flex flex-1 items-center justify-center overflow-auto bg-muted/60 p-6 transition-colors ${
603
+ isDragOver ? 'ring-2 ring-inset ring-primary/40 bg-primary/5' : ''
604
+ }`}
605
+ onDragOver={handleDragOver}
606
+ onDragLeave={handleDragLeave}
607
+ onDrop={handleDrop}
608
+ >
609
+ {isDragOver && (
610
+ <div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center">
611
+ <div className="rounded-lg bg-primary/10 px-4 py-2 text-sm font-medium text-primary backdrop-blur-sm">
612
+ Solte para adicionar ao canvas
613
+ </div>
614
+ </div>
615
+ )}
616
+ {isLoading && (
617
+ <div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center bg-background/70 backdrop-blur-sm">
618
+ <div className="rounded-md border bg-card px-4 py-2 text-sm text-muted-foreground">
619
+ Carregando template...
620
+ </div>
621
+ </div>
622
+ )}
623
+ <div className="shadow-lg" style={{ lineHeight: 0 }}>
624
+ <canvas ref={canvasElRef} />
625
+ </div>
626
+ </div>
627
+ );
628
+ }
629
+
630
+ /* ─────────────────── helpers ─────────────────── */
631
+
632
+ function onSelect(e: any) {
633
+ const obj = e.selected?.[0];
634
+ if (obj?._tplId) {
635
+ useTemplateStore.getState().selectObject(obj._tplId);
636
+ }
637
+ }
638
+
639
+ function onModified(e: any) {
640
+ const obj = e.target ?? e;
641
+ if (!obj?._tplId) return;
642
+ const partial = fabricObjToTemplatePartial(obj);
643
+
644
+ const extra: Partial<TemplateObject> = {};
645
+ if (obj._tplType === 'field' || obj._tplType === 'staticText') {
646
+ extra.text = obj.text;
647
+ }
648
+
649
+ useTemplateStore
650
+ .getState()
651
+ .updateObject(obj._tplId, { ...partial, ...extra });
652
+ }
653
+
654
+ async function applyBg(canvas: any, fabricMod: any, src: string) {
655
+ try {
656
+ const img = await fabricMod.FabricImage.fromURL(src, {
657
+ crossOrigin: 'anonymous',
658
+ });
659
+ const scaleX = CANVAS_W / (img.width || 1);
660
+ const scaleY = CANVAS_H / (img.height || 1);
661
+ img.set({ scaleX, scaleY, selectable: false, evented: false });
662
+ canvas.backgroundImage = img;
663
+ canvas.requestRenderAll();
664
+ } catch {
665
+ /* ignore bad image */
666
+ }
667
+ }
668
+
669
+ /* ────────────── canvas API (for panels) ────────────── */
670
+
671
+ function buildAPI(canvas: any, fabricMod: any): CanvasAPI {
672
+ function findObj(id: string) {
673
+ return canvas.getObjects().find((o: any) => o._tplId === id);
674
+ }
675
+
676
+ function createAndAdd(
677
+ type: 'field' | 'staticText' | 'image' | 'shape',
678
+ cx: number,
679
+ cy: number,
680
+ opts?: { key?: string; shape?: 'rect' | 'line' }
681
+ ) {
682
+ const id = crypto.randomUUID();
683
+ const key = opts?.key;
684
+ const shape = opts?.shape;
685
+ let fObj: any = null;
686
+
687
+ if (type === 'field') {
688
+ fObj = new fabricMod.Textbox(`{{${key}}}`, {
689
+ left: cx - 120,
690
+ top: cy - 18,
691
+ width: 240,
692
+ fontSize: 28,
693
+ fontFamily: 'Inter',
694
+ fontWeight: '400',
695
+ fill: '#1e293b',
696
+ });
697
+ fObj._tplKey = key;
698
+ } else if (type === 'staticText') {
699
+ fObj = new fabricMod.Textbox('Texto', {
700
+ left: cx - 80,
701
+ top: cy - 18,
702
+ width: 200,
703
+ fontSize: 28,
704
+ fontFamily: 'Inter',
705
+ fontWeight: '400',
706
+ fill: '#1e293b',
707
+ });
708
+ } else if (type === 'shape' && shape === 'rect') {
709
+ fObj = new fabricMod.Rect({
710
+ left: cx - 100,
711
+ top: cy - 50,
712
+ width: 200,
713
+ height: 100,
714
+ fill: '#e2e8f0',
715
+ stroke: '#94a3b8',
716
+ strokeWidth: 1,
717
+ });
718
+ } else if (type === 'shape' && shape === 'line') {
719
+ fObj = new fabricMod.Line([0, 0, 300, 0], {
720
+ left: cx - 150,
721
+ top: cy,
722
+ stroke: '#334155',
723
+ strokeWidth: 2,
724
+ });
725
+ } else if (type === 'image') {
726
+ fObj = new fabricMod.Rect({
727
+ left: cx - 75,
728
+ top: cy - 75,
729
+ width: 150,
730
+ height: 150,
731
+ fill: '#e2e8f0',
732
+ stroke: '#cbd5e1',
733
+ strokeWidth: 1,
734
+ rx: 4,
735
+ ry: 4,
736
+ });
737
+ fObj._tplImageSrc = '';
738
+ }
739
+
740
+ if (!fObj) return;
741
+
742
+ fObj._tplId = id;
743
+ fObj._tplType = type;
744
+ fObj._tplKey = key;
745
+ fObj._tplShape = shape;
746
+
747
+ canvas.add(fObj);
748
+ canvas.setActiveObject(fObj);
749
+ canvas.requestRenderAll();
750
+
751
+ const tObj = fabricObjToFull(fObj);
752
+ tObj.id = id;
753
+ tObj.zIndex =
754
+ canvas.getObjects().filter((o: any) => !o._isGuide).length - 1;
755
+
756
+ useTemplateStore.getState().addObject(tObj);
757
+ useTemplateStore.getState().selectObject(id);
758
+ }
759
+
760
+ return {
761
+ addObject(type, opts) {
762
+ createAndAdd(type, CANVAS_W / 2, CANVAS_H / 2, opts);
763
+ },
764
+
765
+ addObjectAt(type, canvasX, canvasY, opts) {
766
+ createAndAdd(type, canvasX, canvasY, opts);
767
+ },
768
+
769
+ removeObject(id) {
770
+ const obj = findObj(id);
771
+ if (obj) {
772
+ canvas.remove(obj);
773
+ canvas.discardActiveObject();
774
+ canvas.requestRenderAll();
775
+ }
776
+ useTemplateStore.getState().removeObject(id);
777
+ useTemplateStore.getState().selectObject(null);
778
+ },
779
+
780
+ duplicateObject(id) {
781
+ const cloneData = useTemplateStore.getState().duplicateObject(id);
782
+ if (!cloneData) return;
783
+ const fObj = createFabricFromTemplate(cloneData, fabricMod);
784
+ if (fObj) {
785
+ canvas.add(fObj);
786
+ canvas.setActiveObject(fObj);
787
+ canvas.requestRenderAll();
788
+ }
789
+ },
790
+
791
+ selectObject(id) {
792
+ const obj = findObj(id);
793
+ if (obj) {
794
+ canvas.setActiveObject(obj);
795
+ canvas.requestRenderAll();
796
+ }
797
+ useTemplateStore.getState().selectObject(id);
798
+ },
799
+
800
+ updateObjectProp(id, fabricProps) {
801
+ const obj = findObj(id);
802
+ if (!obj) return;
803
+
804
+ Object.entries(fabricProps).forEach(([k, v]) => {
805
+ obj.set(k as any, v);
806
+ });
807
+ obj.setCoords?.();
808
+ canvas.requestRenderAll();
809
+
810
+ const full = fabricObjToFull(obj);
811
+ useTemplateStore.getState().updateObject(id, full);
812
+ },
813
+
814
+ async setBackground(dataUrl) {
815
+ await applyBg(canvas, fabricMod, dataUrl);
816
+ useTemplateStore.getState().setBackground(dataUrl);
817
+ },
818
+
819
+ lockObject(id, locked) {
820
+ const obj = findObj(id);
821
+ if (!obj) return;
822
+ obj.set({
823
+ lockMovementX: locked,
824
+ lockMovementY: locked,
825
+ lockRotation: locked,
826
+ lockScalingX: locked,
827
+ lockScalingY: locked,
828
+ hasControls: !locked,
829
+ selectable: !locked,
830
+ evented: !locked,
831
+ });
832
+ if (locked) {
833
+ canvas.discardActiveObject();
834
+ }
835
+ canvas.requestRenderAll();
836
+ useTemplateStore.getState().updateObject(id, { locked });
837
+ },
838
+
839
+ setObjectVisible(id, visible) {
840
+ const obj = findObj(id);
841
+ if (!obj) return;
842
+ obj.set({ visible });
843
+ if (!visible) {
844
+ canvas.discardActiveObject();
845
+ }
846
+ canvas.requestRenderAll();
847
+ useTemplateStore.getState().updateObject(id, { visible });
848
+ },
849
+
850
+ loadTemplate(template) {
851
+ canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
852
+ const { zoom: manualZoom, effectiveZoom } = useTemplateStore.getState();
853
+ const z = Math.min(manualZoom, effectiveZoom);
854
+ applyCanvasZoom(canvas, z);
855
+
856
+ templateJSONToFabric(canvas, template, fabricMod);
857
+ if (template.background.src) {
858
+ applyBg(canvas, fabricMod, template.background.src);
859
+ }
860
+ canvas.requestRenderAll();
861
+ },
862
+
863
+ setZoom(z) {
864
+ useTemplateStore.getState().setZoom(z);
865
+ },
866
+
867
+ reorderObject(id, direction) {
868
+ const obj = findObj(id);
869
+ if (!obj) return;
870
+ if (direction === 'up') {
871
+ canvas.bringObjectForward(obj);
872
+ } else if (direction === 'down') {
873
+ canvas.sendObjectBackwards(obj);
874
+ }
875
+ canvas.requestRenderAll();
876
+
877
+ const ids = canvas
878
+ .getObjects()
879
+ .filter((o: any) => !o._isGuide)
880
+ .map((o: any) => o._tplId)
881
+ .filter(Boolean);
882
+ useTemplateStore.getState().reorderObjects(ids);
883
+ },
884
+
885
+ reorderByIds(idsBottomToTop: string[]) {
886
+ const objMap = new Map<string, any>();
887
+ canvas.getObjects().forEach((o: any) => {
888
+ if (o._tplId && !o._isGuide) objMap.set(o._tplId, o);
889
+ });
890
+ objMap.forEach((o) => canvas.remove(o));
891
+ idsBottomToTop.forEach((id) => {
892
+ const o = objMap.get(id);
893
+ if (o) canvas.add(o);
894
+ });
895
+ canvas.requestRenderAll();
896
+ },
897
+
898
+ clientToCanvas(clientX: number, clientY: number) {
899
+ const el = canvas.getElement?.() ?? canvas.lowerCanvasEl;
900
+ if (!el) return null;
901
+ const rect = el.getBoundingClientRect();
902
+ const vpt = canvas.viewportTransform;
903
+ if (!vpt) return null;
904
+ const sx = clientX - rect.left;
905
+ const sy = clientY - rect.top;
906
+ const invZoom = 1 / vpt[0];
907
+ const x = (sx - vpt[4]) * invZoom;
908
+ const y = (sy - vpt[5]) * invZoom;
909
+ return { x, y };
910
+ },
911
+
912
+ getCanvasElement() {
913
+ return canvas.getElement?.() ?? canvas.lowerCanvasEl ?? null;
914
+ },
915
+ };
916
+ }