@hed-hog/lms 0.0.364 → 0.0.366

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 (290) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/class-group/class-group.controller.d.ts +1 -0
  6. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  7. package/dist/class-group/class-group.service.d.ts +1 -0
  8. package/dist/class-group/class-group.service.d.ts.map +1 -1
  9. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  10. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  12. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  13. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  14. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  15. package/dist/course/course-export-scorm12.service.js +628 -0
  16. package/dist/course/course-export-scorm12.service.js.map +1 -0
  17. package/dist/course/course-export.service.d.ts +84 -0
  18. package/dist/course/course-export.service.d.ts.map +1 -0
  19. package/dist/course/course-export.service.js +237 -0
  20. package/dist/course/course-export.service.js.map +1 -0
  21. package/dist/course/course-structure.controller.d.ts +20 -10
  22. package/dist/course/course-structure.controller.d.ts.map +1 -1
  23. package/dist/course/course-structure.controller.js +20 -4
  24. package/dist/course/course-structure.controller.js.map +1 -1
  25. package/dist/course/course-structure.service.d.ts +12 -4
  26. package/dist/course/course-structure.service.d.ts.map +1 -1
  27. package/dist/course/course-structure.service.js +98 -23
  28. package/dist/course/course-structure.service.js.map +1 -1
  29. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  30. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  31. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  32. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  33. package/dist/course/course-video-hls.service.d.ts +71 -0
  34. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  35. package/dist/course/course-video-hls.service.js +784 -0
  36. package/dist/course/course-video-hls.service.js.map +1 -0
  37. package/dist/course/course.controller.d.ts +47 -13
  38. package/dist/course/course.controller.d.ts.map +1 -1
  39. package/dist/course/course.controller.js +40 -26
  40. package/dist/course/course.controller.js.map +1 -1
  41. package/dist/course/course.mcp-tools.js +1 -1
  42. package/dist/course/course.mcp-tools.js.map +1 -1
  43. package/dist/course/course.module.d.ts.map +1 -1
  44. package/dist/course/course.module.js +16 -0
  45. package/dist/course/course.module.js.map +1 -1
  46. package/dist/course/course.service.d.ts +8 -9
  47. package/dist/course/course.service.d.ts.map +1 -1
  48. package/dist/course/course.service.js +93 -50
  49. package/dist/course/course.service.js.map +1 -1
  50. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  51. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  52. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  53. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  54. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  55. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  56. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  57. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  58. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  59. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  62. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  63. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  64. package/dist/course/dto/create-course-export.dto.js +71 -0
  65. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  66. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  67. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  68. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  69. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  70. package/dist/course/ffmpeg.util.d.ts +10 -0
  71. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  72. package/dist/course/ffmpeg.util.js +79 -0
  73. package/dist/course/ffmpeg.util.js.map +1 -0
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts +18 -1
  75. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload-automation.service.js +106 -8
  77. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  78. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  79. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  80. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  81. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  82. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  83. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  84. package/dist/course/lms-bulk-upload.controller.js +43 -2
  85. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  86. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  87. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  88. package/dist/course/lms-bulk-upload.service.js +59 -6
  89. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  90. package/dist/course/lms-setting.controller.d.ts +2 -1
  91. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  92. package/dist/course/lms-setting.controller.js +4 -2
  93. package/dist/course/lms-setting.controller.js.map +1 -1
  94. package/dist/course/scorm12-schemas.d.ts +4 -0
  95. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  96. package/dist/course/scorm12-schemas.js +9 -0
  97. package/dist/course/scorm12-schemas.js.map +1 -0
  98. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  99. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  100. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  101. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  102. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  103. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  104. package/dist/enterprise/training/training-student.service.js +217 -4
  105. package/dist/enterprise/training/training-student.service.js.map +1 -1
  106. package/dist/evaluation/evaluation.service.d.ts +18 -0
  107. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  108. package/dist/evaluation/evaluation.service.js +125 -0
  109. package/dist/evaluation/evaluation.service.js.map +1 -1
  110. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  111. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  112. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  113. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  114. package/dist/exam/exam.module.d.ts.map +1 -1
  115. package/dist/exam/exam.module.js +2 -1
  116. package/dist/exam/exam.module.js.map +1 -1
  117. package/dist/exam/exam.service.d.ts +21 -0
  118. package/dist/exam/exam.service.d.ts.map +1 -1
  119. package/dist/exam/exam.service.js +80 -0
  120. package/dist/exam/exam.service.js.map +1 -1
  121. package/dist/exam/question.controller.d.ts +27 -0
  122. package/dist/exam/question.controller.d.ts.map +1 -0
  123. package/dist/exam/question.controller.js +53 -0
  124. package/dist/exam/question.controller.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  126. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  127. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  128. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  129. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  130. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  131. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  132. package/dist/lms-commerce-access.subscriber.js +74 -0
  133. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +16 -5
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  138. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  140. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  141. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  142. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  143. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  144. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  145. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  146. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  147. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  148. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  149. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  150. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  151. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  152. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  153. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  154. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  155. package/dist/platforma/platforma-performance.service.js +500 -0
  156. package/dist/platforma/platforma-performance.service.js.map +1 -0
  157. package/dist/platforma/platforma-search.service.d.ts +21 -0
  158. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  159. package/dist/platforma/platforma-search.service.js +64 -0
  160. package/dist/platforma/platforma-search.service.js.map +1 -0
  161. package/dist/platforma/platforma-video.service.d.ts +39 -0
  162. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  163. package/dist/platforma/platforma-video.service.js +301 -0
  164. package/dist/platforma/platforma-video.service.js.map +1 -0
  165. package/dist/platforma/platforma.controller.d.ts +209 -1
  166. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  167. package/dist/platforma/platforma.controller.js +208 -2
  168. package/dist/platforma/platforma.controller.js.map +1 -1
  169. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  170. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  171. package/dist/realtime/lms-realtime.controller.js +31 -0
  172. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  173. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  174. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  175. package/dist/realtime/lms-realtime.service.js.map +1 -1
  176. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  177. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  178. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  179. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  180. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  181. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  182. package/dist/student-xp/student-xp.controller.js +24 -0
  183. package/dist/student-xp/student-xp.controller.js.map +1 -1
  184. package/dist/student-xp/student-xp.service.d.ts +16 -0
  185. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  186. package/dist/student-xp/student-xp.service.js +51 -1
  187. package/dist/student-xp/student-xp.service.js.map +1 -1
  188. package/hedhog/data/evaluation_topic.yaml +17 -0
  189. package/hedhog/data/menu.yaml +0 -17
  190. package/hedhog/data/queue_definition.yaml +48 -0
  191. package/hedhog/data/route.yaml +94 -124
  192. package/hedhog/data/setting_group.yaml +19 -19
  193. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  194. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  195. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  196. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  197. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  198. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  199. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  200. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +158 -45
  201. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  202. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  203. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  204. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  205. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  206. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  207. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  208. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  209. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  210. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  211. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  212. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  213. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  214. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  215. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  216. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -1
  217. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  218. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  219. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  220. package/hedhog/frontend/app/courses/page.tsx.ejs +85 -9
  221. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  222. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  223. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  224. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  225. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  226. package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
  227. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  228. package/hedhog/frontend/messages/en.json +44 -28
  229. package/hedhog/frontend/messages/pt.json +47 -29
  230. package/hedhog/table/course_enrollment.yaml +3 -0
  231. package/hedhog/table/course_export.yaml +62 -0
  232. package/hedhog/table/lesson_view_event.yaml +66 -0
  233. package/package.json +14 -9
  234. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  235. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  236. package/src/course/course-export-scorm12.service.ts +668 -0
  237. package/src/course/course-export.service.ts +280 -0
  238. package/src/course/course-structure.controller.ts +16 -2
  239. package/src/course/course-structure.service.ts +100 -7
  240. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  241. package/src/course/course-video-hls.service.ts +966 -0
  242. package/src/course/course.controller.ts +33 -19
  243. package/src/course/course.mcp-tools.ts +1 -1
  244. package/src/course/course.module.ts +16 -0
  245. package/src/course/course.service.ts +119 -61
  246. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  247. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  248. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  249. package/src/course/dto/create-course-export.dto.ts +56 -0
  250. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  251. package/src/course/ffmpeg.util.ts +65 -0
  252. package/src/course/lms-bulk-upload-automation.service.ts +156 -6
  253. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  254. package/src/course/lms-bulk-upload.controller.ts +32 -2
  255. package/src/course/lms-bulk-upload.service.ts +70 -7
  256. package/src/course/lms-setting.controller.ts +4 -2
  257. package/src/course/scorm12-schemas.ts +9 -0
  258. package/src/enterprise/training/training-student.service.ts +221 -2
  259. package/src/evaluation/evaluation.service.ts +123 -0
  260. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  261. package/src/exam/exam.module.ts +2 -1
  262. package/src/exam/exam.service.ts +86 -0
  263. package/src/exam/question.controller.ts +28 -0
  264. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  265. package/src/lms-commerce-access.subscriber.ts +88 -0
  266. package/src/lms.module.ts +16 -5
  267. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  268. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  269. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  270. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  271. package/src/platforma/platforma-performance.service.ts +606 -0
  272. package/src/platforma/platforma-search.service.ts +48 -0
  273. package/src/platforma/platforma-video.service.ts +346 -0
  274. package/src/platforma/platforma.controller.ts +137 -1
  275. package/src/platforma/platforma.service.ts +268 -268
  276. package/src/realtime/lms-realtime.controller.ts +27 -1
  277. package/src/realtime/lms-realtime.service.ts +2 -1
  278. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  279. package/src/student-xp/student-xp.controller.ts +18 -2
  280. package/src/student-xp/student-xp.service.ts +84 -2
  281. package/hedhog/data/video_resolution_profile.yaml +0 -7
  282. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  283. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  284. package/hedhog/table/video_resolution_profile.yaml +0 -18
  285. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  286. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  287. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  288. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  289. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  290. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -0,0 +1,606 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable } from '@nestjs/common';
3
+
4
+ const CATEGORY_COLORS = [
5
+ '#8B5CF6',
6
+ '#22C55E',
7
+ '#3B82F6',
8
+ '#F59E0B',
9
+ '#EC4899',
10
+ '#14B8A6',
11
+ ];
12
+
13
+ const TRAIL_COLORS = [
14
+ '#8B5CF6',
15
+ '#34D399',
16
+ '#F59E0B',
17
+ '#60A5FA',
18
+ '#FB7185',
19
+ '#A78BFA',
20
+ ];
21
+
22
+ const PT_MONTHS: Record<string, string> = {
23
+ Jan: 'Jan',
24
+ Feb: 'Fev',
25
+ Mar: 'Mar',
26
+ Apr: 'Abr',
27
+ May: 'Mai',
28
+ Jun: 'Jun',
29
+ Jul: 'Jul',
30
+ Aug: 'Ago',
31
+ Sep: 'Set',
32
+ Oct: 'Out',
33
+ Nov: 'Nov',
34
+ Dec: 'Dez',
35
+ };
36
+
37
+ function slugToTitle(slug: string): string {
38
+ return slug
39
+ .split('-')
40
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
41
+ .join(' ');
42
+ }
43
+
44
+ function toPtMonth(enAbbr: string): string {
45
+ return PT_MONTHS[enAbbr] ?? enAbbr;
46
+ }
47
+
48
+ function parsePeriod(period: string): {
49
+ start: Date;
50
+ previousStart: Date;
51
+ previousEnd: Date;
52
+ } {
53
+ const now = new Date();
54
+ const start = new Date(now);
55
+
56
+ if (period === '3m') {
57
+ start.setMonth(start.getMonth() - 3);
58
+ } else if (period === 'year') {
59
+ start.setMonth(0, 1);
60
+ start.setHours(0, 0, 0, 0);
61
+ } else {
62
+ // default: 30d
63
+ start.setDate(start.getDate() - 30);
64
+ }
65
+
66
+ const periodMs = now.getTime() - start.getTime();
67
+ const previousEnd = new Date(start);
68
+ const previousStart = new Date(start.getTime() - periodMs);
69
+
70
+ return { start, previousStart, previousEnd };
71
+ }
72
+
73
+ function formatHms(totalSeconds: number): string {
74
+ const h = Math.floor(totalSeconds / 3600);
75
+ const m = Math.floor((totalSeconds % 3600) / 60);
76
+ if (h > 0) return `${h}h ${m}m`;
77
+ return `${m}m`;
78
+ }
79
+
80
+ function calcChange(current: number, previous: number): number {
81
+ if (previous === 0) return current > 0 ? 100 : 0;
82
+ return Math.round(((current - previous) / previous) * 100);
83
+ }
84
+
85
+ @Injectable()
86
+ export class PlatformaPerformanceService {
87
+ constructor(private readonly prisma: PrismaService) {}
88
+
89
+ async getSummary(userId: number, period: string) {
90
+ const { start, previousStart, previousEnd } = parsePeriod(period);
91
+
92
+ const personId = await this.getPersonId(userId);
93
+
94
+ const [kpis, studyHours, areaPerformance, accuracy, lessonsByCategory, avgLessonTime, trailProgress, ranking] =
95
+ await Promise.all([
96
+ this.getKpis(userId, personId, start, previousStart, previousEnd),
97
+ this.getStudyHoursChart(personId),
98
+ this.getAreaPerformance(personId),
99
+ this.getAccuracyDistribution(personId, start),
100
+ this.getLessonsByCategory(personId, start),
101
+ this.getAvgLessonTime(personId),
102
+ this.getTrailProgress(personId),
103
+ this.getRanking(),
104
+ ]);
105
+
106
+ return {
107
+ kpis,
108
+ studyHours,
109
+ areaPerformance,
110
+ accuracyDistribution: accuracy,
111
+ lessonsByCategory,
112
+ avgLessonTime,
113
+ trailProgress,
114
+ ranking,
115
+ };
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // KPI cards
120
+ // ---------------------------------------------------------------------------
121
+
122
+ private async getKpis(
123
+ userId: number,
124
+ personId: number | null,
125
+ start: Date,
126
+ previousStart: Date,
127
+ previousEnd: Date,
128
+ ) {
129
+ const [
130
+ studyTimeCurrent,
131
+ studyTimePrev,
132
+ lessonsCurrent,
133
+ lessonsPrev,
134
+ exercisesCurrent,
135
+ exercisesPrev,
136
+ accuracyCurrent,
137
+ accuracyPrev,
138
+ bitcodes,
139
+ bitcodesEarnedCurrent,
140
+ bitcodesEarnedPrev,
141
+ studySparkline,
142
+ lessonsSparkline,
143
+ exercisesSparkline,
144
+ bitcodesSparkline,
145
+ ] = await Promise.all([
146
+ this.countStudySeconds(userId, start, new Date()),
147
+ this.countStudySeconds(userId, previousStart, previousEnd),
148
+ this.countCompletedLessons(personId, start, new Date()),
149
+ this.countCompletedLessons(personId, previousStart, previousEnd),
150
+ this.countResolvedExercises(personId, start, new Date()),
151
+ this.countResolvedExercises(personId, previousStart, previousEnd),
152
+ this.getAccuracyRate(personId, start, new Date()),
153
+ this.getAccuracyRate(personId, previousStart, previousEnd),
154
+ this.getBitcodesBalance(personId),
155
+ this.getBitcodesEarned(personId, start, new Date()),
156
+ this.getBitcodesEarned(personId, previousStart, previousEnd),
157
+ this.getWeeklySparkline((w) => this.countStudySeconds(userId, w.start, w.end), 10),
158
+ this.getWeeklySparkline((w) => this.countCompletedLessons(personId, w.start, w.end), 10),
159
+ this.getWeeklySparkline((w) => this.countResolvedExercises(personId, w.start, w.end), 10),
160
+ this.getWeeklySparkline((w) => this.getBitcodesEarned(personId, w.start, w.end), 10),
161
+ ]);
162
+
163
+ const accuracyChange = calcChange(accuracyCurrent, accuracyPrev);
164
+
165
+ return [
166
+ {
167
+ id: 'study-time',
168
+ title: 'Tempo de estudo',
169
+ value: formatHms(studyTimeCurrent),
170
+ subtitle: 'Horas totais',
171
+ change: this.fmtChange(calcChange(studyTimeCurrent, studyTimePrev)),
172
+ icon: 'clock' as const,
173
+ color: '#8B5CF6',
174
+ sparkline: studySparkline,
175
+ },
176
+ {
177
+ id: 'completed-lessons',
178
+ title: 'Aulas concluídas',
179
+ value: String(lessonsCurrent),
180
+ subtitle: 'Total de aulas',
181
+ change: this.fmtChange(calcChange(lessonsCurrent, lessonsPrev)),
182
+ icon: 'bookOpen' as const,
183
+ color: '#22C55E',
184
+ sparkline: lessonsSparkline,
185
+ },
186
+ {
187
+ id: 'resolved-exercises',
188
+ title: 'Exercícios resolvidos',
189
+ value: String(exercisesCurrent),
190
+ subtitle: 'Total de exercícios',
191
+ change: this.fmtChange(calcChange(exercisesCurrent, exercisesPrev)),
192
+ icon: 'pencil' as const,
193
+ color: '#FB923C',
194
+ sparkline: exercisesSparkline,
195
+ },
196
+ {
197
+ id: 'accuracy-rate',
198
+ title: 'Taxa de acerto',
199
+ value: `${accuracyCurrent}%`,
200
+ subtitle: 'Média de acertos',
201
+ change: this.fmtChange(accuracyChange),
202
+ icon: 'target' as const,
203
+ color: '#3B82F6',
204
+ sparkline: await this.getWeeklySparkline(
205
+ (w) => this.getAccuracyRate(personId, w.start, w.end),
206
+ 10,
207
+ ),
208
+ },
209
+ {
210
+ id: 'bitcodes',
211
+ title: 'Bitcodes',
212
+ value: bitcodes.toLocaleString('pt-BR'),
213
+ subtitle: 'Saldo atual',
214
+ change: this.fmtBitcodesChange(bitcodesEarnedCurrent, bitcodesEarnedPrev),
215
+ icon: 'coins' as const,
216
+ color: '#F59E0B',
217
+ sparkline: bitcodesSparkline,
218
+ },
219
+ ];
220
+ }
221
+
222
+ private fmtChange(pct: number): string {
223
+ const sign = pct >= 0 ? '+' : '';
224
+ return `${sign}${pct}% vs. mês passado`;
225
+ }
226
+
227
+ private fmtBitcodesChange(current: number, previous: number): string {
228
+ const diff = current - previous;
229
+ const sign = diff >= 0 ? '+' : '';
230
+ return `${sign}${diff.toLocaleString('pt-BR')} vs. mês passado`;
231
+ }
232
+
233
+ private async countStudySeconds(userId: number, start: Date, end: Date): Promise<number> {
234
+ const rows = await (this.prisma as any).$queryRaw<[{ total: bigint }]>`
235
+ SELECT COUNT(*) * 15 AS total
236
+ FROM lesson_view_event
237
+ WHERE user_id = ${userId}
238
+ AND created_at >= ${start}
239
+ AND created_at < ${end}
240
+ `;
241
+ return Number(rows[0]?.total ?? 0);
242
+ }
243
+
244
+ private async countCompletedLessons(
245
+ personId: number | null,
246
+ start: Date,
247
+ end: Date,
248
+ ): Promise<number> {
249
+ if (!personId) return 0;
250
+ const rows = await (this.prisma as any).$queryRaw<[{ total: bigint }]>`
251
+ SELECT COUNT(*) AS total
252
+ FROM course_lesson_progress clp
253
+ JOIN course_enrollment ce ON ce.id = clp.course_enrollment_id
254
+ WHERE ce.person_id = ${personId}
255
+ AND clp.status = 'completed'
256
+ AND clp.completed_at >= ${start}
257
+ AND clp.completed_at < ${end}
258
+ `;
259
+ return Number(rows[0]?.total ?? 0);
260
+ }
261
+
262
+ private async countResolvedExercises(
263
+ personId: number | null,
264
+ start: Date,
265
+ end: Date,
266
+ ): Promise<number> {
267
+ if (!personId) return 0;
268
+ const rows = await (this.prisma as any).$queryRaw<[{ total: bigint }]>`
269
+ SELECT COUNT(ea.id) AS total
270
+ FROM exam_answer ea
271
+ JOIN exam_attempt att ON att.id = ea.exam_attempt_id
272
+ WHERE att.student_id = ${personId}
273
+ AND att.started_at >= ${start}
274
+ AND att.started_at < ${end}
275
+ `;
276
+ return Number(rows[0]?.total ?? 0);
277
+ }
278
+
279
+ private async getAccuracyRate(
280
+ personId: number | null,
281
+ start: Date,
282
+ end: Date,
283
+ ): Promise<number> {
284
+ if (!personId) return 0;
285
+ const rows = await (this.prisma as any).$queryRaw<
286
+ [{ correct: bigint; total: bigint }]
287
+ >`
288
+ SELECT
289
+ COUNT(CASE WHEN ea.is_correct = true THEN 1 END) AS correct,
290
+ COUNT(*) AS total
291
+ FROM exam_answer ea
292
+ JOIN exam_attempt att ON att.id = ea.exam_attempt_id
293
+ WHERE att.student_id = ${personId}
294
+ AND ea.is_correct IS NOT NULL
295
+ AND att.started_at >= ${start}
296
+ AND att.started_at < ${end}
297
+ `;
298
+ const { correct, total } = rows[0] ?? { correct: BigInt(0), total: BigInt(0) };
299
+ const t = Number(total);
300
+ return t === 0 ? 0 : Math.round((Number(correct) / t) * 100);
301
+ }
302
+
303
+ private async getBitcodesBalance(personId: number | null): Promise<number> {
304
+ if (!personId) return 0;
305
+ const row = await (this.prisma as any).bitcode_wallet.findFirst({
306
+ where: { person_id: personId },
307
+ select: { current_balance: true },
308
+ });
309
+ return Number(row?.current_balance ?? 0);
310
+ }
311
+
312
+ private async getBitcodesEarned(
313
+ personId: number | null,
314
+ start: Date,
315
+ end: Date,
316
+ ): Promise<number> {
317
+ if (!personId) return 0;
318
+ const rows = await (this.prisma as any).$queryRaw<[{ total: bigint }]>`
319
+ SELECT COALESCE(SUM(bwt.amount), 0) AS total
320
+ FROM bitcode_wallet_transaction bwt
321
+ JOIN bitcode_wallet bw ON bw.id = bwt.wallet_id
322
+ WHERE bw.person_id = ${personId}
323
+ AND bwt.type = 'credit'
324
+ AND bwt.created_at >= ${start}
325
+ AND bwt.created_at < ${end}
326
+ `;
327
+ return Number(rows[0]?.total ?? 0);
328
+ }
329
+
330
+ private async getWeeklySparkline(
331
+ fetcher: (w: { start: Date; end: Date }) => Promise<number>,
332
+ weeks: number,
333
+ ): Promise<Array<{ index: number; value: number }>> {
334
+ const now = new Date();
335
+ const results: Array<{ index: number; value: number }> = [];
336
+
337
+ for (let i = weeks - 1; i >= 0; i--) {
338
+ const end = new Date(now.getTime() - i * 7 * 24 * 3600 * 1000);
339
+ const start = new Date(end.getTime() - 7 * 24 * 3600 * 1000);
340
+ const value = await fetcher({ start, end });
341
+ results.push({ index: weeks - i, value });
342
+ }
343
+
344
+ return results;
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Study hours chart
349
+ // ---------------------------------------------------------------------------
350
+
351
+ private async getStudyHoursChart(
352
+ personId: number | null,
353
+ ): Promise<Array<{ month: string; hours: number }>> {
354
+ if (!personId) return [];
355
+
356
+ const tenMonthsAgo = new Date();
357
+ tenMonthsAgo.setMonth(tenMonthsAgo.getMonth() - 10);
358
+
359
+ const rows = await (this.prisma as any).$queryRaw<
360
+ Array<{ month_label: string; hours: number }>
361
+ >`
362
+ SELECT
363
+ TO_CHAR(DATE_TRUNC('month', clp.completed_at), 'Mon') AS month_label,
364
+ ROUND(SUM(cl.duration_seconds) / 3600.0, 1) AS hours
365
+ FROM course_lesson_progress clp
366
+ JOIN course_enrollment ce ON ce.id = clp.course_enrollment_id
367
+ JOIN course_lesson cl ON cl.id = clp.course_lesson_id
368
+ WHERE ce.person_id = ${personId}
369
+ AND clp.status = 'completed'
370
+ AND clp.completed_at IS NOT NULL
371
+ AND clp.completed_at >= ${tenMonthsAgo}
372
+ GROUP BY DATE_TRUNC('month', clp.completed_at)
373
+ ORDER BY DATE_TRUNC('month', clp.completed_at)
374
+ `;
375
+
376
+ return rows.map((r) => ({
377
+ month: toPtMonth(r.month_label),
378
+ hours: Number(r.hours),
379
+ }));
380
+ }
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Area radar chart
384
+ // ---------------------------------------------------------------------------
385
+
386
+ private async getAreaPerformance(
387
+ personId: number | null,
388
+ ): Promise<Array<{ area: string; score: number }>> {
389
+ if (!personId) return [];
390
+
391
+ const rows = await (this.prisma as any).$queryRaw<
392
+ Array<{ slug: string; score: number }>
393
+ >`
394
+ SELECT
395
+ cat.slug AS slug,
396
+ ROUND(AVG(ce.progress_percent)) AS score
397
+ FROM course_enrollment ce
398
+ JOIN course_category cc ON cc.course_id = ce.course_id
399
+ JOIN category cat ON cat.id = cc.category_id
400
+ WHERE ce.person_id = ${personId}
401
+ GROUP BY cat.id, cat.slug
402
+ ORDER BY score DESC
403
+ LIMIT 6
404
+ `;
405
+
406
+ return rows.map((r) => ({
407
+ area: slugToTitle(r.slug),
408
+ score: Number(r.score),
409
+ }));
410
+ }
411
+
412
+ // ---------------------------------------------------------------------------
413
+ // Accuracy donut chart
414
+ // ---------------------------------------------------------------------------
415
+
416
+ private async getAccuracyDistribution(
417
+ personId: number | null,
418
+ start: Date,
419
+ ): Promise<Array<{ label: string; value: number; fill: string }>> {
420
+ if (!personId) {
421
+ return [
422
+ { label: 'Acertos', value: 0, fill: '#4ADE80' },
423
+ { label: 'Erros', value: 0, fill: '#FB7185' },
424
+ { label: 'Não respondidas', value: 0, fill: '#D1D5DB' },
425
+ ];
426
+ }
427
+
428
+ const rows = await (this.prisma as any).$queryRaw<
429
+ [{ correct: bigint; incorrect: bigint; unanswered: bigint }]
430
+ >`
431
+ SELECT
432
+ COUNT(CASE WHEN ea.is_correct = true THEN 1 END) AS correct,
433
+ COUNT(CASE WHEN ea.is_correct = false THEN 1 END) AS incorrect,
434
+ COUNT(CASE WHEN ea.is_correct IS NULL THEN 1 END) AS unanswered
435
+ FROM exam_answer ea
436
+ JOIN exam_attempt att ON att.id = ea.exam_attempt_id
437
+ WHERE att.student_id = ${personId}
438
+ AND att.started_at >= ${start}
439
+ `;
440
+
441
+ const { correct, incorrect, unanswered } = rows[0] ?? {
442
+ correct: BigInt(0),
443
+ incorrect: BigInt(0),
444
+ unanswered: BigInt(0),
445
+ };
446
+ const total = Number(correct) + Number(incorrect) + Number(unanswered);
447
+ const pct = (n: bigint) =>
448
+ total === 0 ? 0 : Math.round((Number(n) / total) * 100);
449
+
450
+ return [
451
+ { label: 'Acertos', value: pct(correct), fill: '#4ADE80' },
452
+ { label: 'Erros', value: pct(incorrect), fill: '#FB7185' },
453
+ { label: 'Não respondidas', value: pct(unanswered), fill: '#D1D5DB' },
454
+ ];
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Lessons by category chart
459
+ // ---------------------------------------------------------------------------
460
+
461
+ private async getLessonsByCategory(
462
+ personId: number | null,
463
+ start: Date,
464
+ ): Promise<Array<{ label: string; value: number; fill: string }>> {
465
+ if (!personId) return [];
466
+
467
+ const rows = await (this.prisma as any).$queryRaw<
468
+ Array<{ slug: string; value: bigint }>
469
+ >`
470
+ SELECT
471
+ cat.slug AS slug,
472
+ COUNT(clp.id) AS value
473
+ FROM course_lesson_progress clp
474
+ JOIN course_enrollment ce ON ce.id = clp.course_enrollment_id
475
+ JOIN course_lesson cl ON cl.id = clp.course_lesson_id
476
+ JOIN course_module cm ON cm.id = cl.course_module_id
477
+ JOIN course_category cc ON cc.course_id = cm.course_id
478
+ JOIN category cat ON cat.id = cc.category_id
479
+ WHERE ce.person_id = ${personId}
480
+ AND clp.status = 'completed'
481
+ AND clp.completed_at >= ${start}
482
+ GROUP BY cat.id, cat.slug
483
+ ORDER BY value DESC
484
+ LIMIT 6
485
+ `;
486
+
487
+ return rows.map((r, i) => ({
488
+ label: slugToTitle(r.slug),
489
+ value: Number(r.value),
490
+ fill: CATEGORY_COLORS[i % CATEGORY_COLORS.length],
491
+ }));
492
+ }
493
+
494
+ // ---------------------------------------------------------------------------
495
+ // Average lesson time chart
496
+ // ---------------------------------------------------------------------------
497
+
498
+ private async getAvgLessonTime(
499
+ personId: number | null,
500
+ ): Promise<Array<{ month: string; minutes: number }>> {
501
+ if (!personId) return [];
502
+
503
+ const tenMonthsAgo = new Date();
504
+ tenMonthsAgo.setMonth(tenMonthsAgo.getMonth() - 10);
505
+
506
+ const rows = await (this.prisma as any).$queryRaw<
507
+ Array<{ month_label: string; minutes: number }>
508
+ >`
509
+ SELECT
510
+ TO_CHAR(DATE_TRUNC('month', clp.completed_at), 'Mon') AS month_label,
511
+ ROUND(AVG(cl.duration_seconds) / 60.0) AS minutes
512
+ FROM course_lesson_progress clp
513
+ JOIN course_enrollment ce ON ce.id = clp.course_enrollment_id
514
+ JOIN course_lesson cl ON cl.id = clp.course_lesson_id
515
+ WHERE ce.person_id = ${personId}
516
+ AND clp.status = 'completed'
517
+ AND clp.completed_at IS NOT NULL
518
+ AND clp.completed_at >= ${tenMonthsAgo}
519
+ GROUP BY DATE_TRUNC('month', clp.completed_at)
520
+ ORDER BY DATE_TRUNC('month', clp.completed_at)
521
+ `;
522
+
523
+ return rows.map((r) => ({
524
+ month: toPtMonth(r.month_label),
525
+ minutes: Number(r.minutes),
526
+ }));
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // Trail progress card
531
+ // ---------------------------------------------------------------------------
532
+
533
+ private async getTrailProgress(
534
+ personId: number | null,
535
+ ): Promise<Array<{ trail: string; progress: number; color: string }>> {
536
+ if (!personId) return [];
537
+
538
+ const rows = await (this.prisma as any).$queryRaw<
539
+ Array<{ title: string; progress: number }>
540
+ >`
541
+ SELECT
542
+ c.title AS title,
543
+ ce.progress_percent AS progress
544
+ FROM course_enrollment ce
545
+ JOIN course c ON c.id = ce.course_id
546
+ WHERE ce.person_id = ${personId}
547
+ AND ce.status IN ('active', 'completed')
548
+ AND ce.progress_percent > 0
549
+ ORDER BY ce.progress_percent DESC
550
+ LIMIT 5
551
+ `;
552
+
553
+ return rows.map((r, i) => ({
554
+ trail: r.title,
555
+ progress: Number(r.progress),
556
+ color: TRAIL_COLORS[i % TRAIL_COLORS.length],
557
+ }));
558
+ }
559
+
560
+ // ---------------------------------------------------------------------------
561
+ // Weekly ranking
562
+ // ---------------------------------------------------------------------------
563
+
564
+ private async getRanking(): Promise<
565
+ Array<{
566
+ id: string;
567
+ position: number;
568
+ name: string;
569
+ score: string;
570
+ avatar: string | null;
571
+ }>
572
+ > {
573
+ const rows = await (this.prisma as any).$queryRaw<
574
+ Array<{ person_id: number; name: string; current_balance: number }>
575
+ >`
576
+ SELECT
577
+ bw.person_id,
578
+ p.name,
579
+ bw.current_balance
580
+ FROM bitcode_wallet bw
581
+ JOIN person p ON p.id = bw.person_id
582
+ ORDER BY bw.current_balance DESC
583
+ LIMIT 5
584
+ `;
585
+
586
+ return rows.map((r, i) => ({
587
+ id: String(r.person_id),
588
+ position: i + 1,
589
+ name: r.name ?? 'Aluno',
590
+ score: `${Number(r.current_balance).toLocaleString('pt-BR')} BITCODES`,
591
+ avatar: null,
592
+ }));
593
+ }
594
+
595
+ // ---------------------------------------------------------------------------
596
+ // Helpers
597
+ // ---------------------------------------------------------------------------
598
+
599
+ private async getPersonId(userId: number): Promise<number | null> {
600
+ const row = await this.prisma.person_user.findFirst({
601
+ where: { user_id: userId },
602
+ select: { person_id: true },
603
+ });
604
+ return row?.person_id ?? null;
605
+ }
606
+ }
@@ -0,0 +1,48 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { ClassGroupService } from '../class-group/class-group.service';
3
+ import { TrainingStudentService } from '../enterprise/training/training-student.service';
4
+ import { ExamService } from '../exam/exam.service';
5
+ import { InstructorService } from '../instructor/instructor.service';
6
+ import { TrainingService } from '../training/training.service';
7
+
8
+ @Injectable()
9
+ export class PlatformaSearchService {
10
+ constructor(
11
+ private readonly trainingStudentService: TrainingStudentService,
12
+ private readonly trainingService: TrainingService,
13
+ private readonly examService: ExamService,
14
+ private readonly classGroupService: ClassGroupService,
15
+ private readonly instructorService: InstructorService,
16
+ ) {}
17
+
18
+ async search(q: string, types: string[], pageSize = 5) {
19
+ const all = types.length === 0;
20
+
21
+ const [courses, tracks, exams, classrooms, instructors] =
22
+ await Promise.allSettled([
23
+ all || types.includes('course')
24
+ ? this.trainingStudentService.getPublishedCourses({ search: q, pageSize })
25
+ : Promise.resolve(null),
26
+ all || types.includes('track')
27
+ ? this.trainingService.list({ search: q, status: 'ativa', pageSize })
28
+ : Promise.resolve(null),
29
+ all || types.includes('exam')
30
+ ? this.examService.list({ search: q, status: 'published', pageSize })
31
+ : Promise.resolve(null),
32
+ all || types.includes('classroom')
33
+ ? this.classGroupService.list({ search: q, pageSize })
34
+ : Promise.resolve(null),
35
+ all || types.includes('instructor')
36
+ ? this.instructorService.list({ search: q, status: 'active', pageSize })
37
+ : Promise.resolve(null),
38
+ ]);
39
+
40
+ return {
41
+ courses: courses.status === 'fulfilled' ? courses.value : null,
42
+ tracks: tracks.status === 'fulfilled' ? tracks.value : null,
43
+ exams: exams.status === 'fulfilled' ? exams.value : null,
44
+ classrooms: classrooms.status === 'fulfilled' ? classrooms.value : null,
45
+ instructors: instructors.status === 'fulfilled' ? instructors.value : null,
46
+ };
47
+ }
48
+ }