@hed-hog/lms 0.0.365 → 0.0.370

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 (243) hide show
  1. package/dist/certificate/certificate.controller.d.ts +1 -1
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +4 -2
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +50 -0
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +73 -0
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/class-group/class-group.controller.d.ts +1 -0
  10. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  11. package/dist/class-group/class-group.service.d.ts +1 -0
  12. package/dist/class-group/class-group.service.d.ts.map +1 -1
  13. package/dist/course/course-ai-usage.service.d.ts +58 -0
  14. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  15. package/dist/course/course-ai-usage.service.js +176 -0
  16. package/dist/course/course-ai-usage.service.js.map +1 -0
  17. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  18. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  19. package/dist/course/course-audio-transcription.service.js +381 -29
  20. package/dist/course/course-audio-transcription.service.js.map +1 -1
  21. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  22. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  23. package/dist/course/course-export-scorm12.service.js +141 -6
  24. package/dist/course/course-export-scorm12.service.js.map +1 -1
  25. package/dist/course/course-export.service.d.ts.map +1 -1
  26. package/dist/course/course-export.service.js +2 -1
  27. package/dist/course/course-export.service.js.map +1 -1
  28. package/dist/course/course-lesson.controller.d.ts +25 -3
  29. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  30. package/dist/course/course-lesson.controller.js +71 -8
  31. package/dist/course/course-lesson.controller.js.map +1 -1
  32. package/dist/course/course-structure.controller.d.ts +30 -7
  33. package/dist/course/course-structure.controller.d.ts.map +1 -1
  34. package/dist/course/course-structure.controller.js +37 -4
  35. package/dist/course/course-structure.controller.js.map +1 -1
  36. package/dist/course/course-structure.service.d.ts +37 -5
  37. package/dist/course/course-structure.service.d.ts.map +1 -1
  38. package/dist/course/course-structure.service.js +165 -20
  39. package/dist/course/course-structure.service.js.map +1 -1
  40. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  41. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  42. package/dist/course/course-transcription-translation.service.js +227 -0
  43. package/dist/course/course-transcription-translation.service.js.map +1 -0
  44. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  45. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  46. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  47. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  48. package/dist/course/course-video-hls.service.d.ts +14 -0
  49. package/dist/course/course-video-hls.service.d.ts.map +1 -1
  50. package/dist/course/course-video-hls.service.js +25 -8
  51. package/dist/course/course-video-hls.service.js.map +1 -1
  52. package/dist/course/course.controller.d.ts +2 -0
  53. package/dist/course/course.controller.d.ts.map +1 -1
  54. package/dist/course/course.module.d.ts.map +1 -1
  55. package/dist/course/course.module.js +9 -0
  56. package/dist/course/course.module.js.map +1 -1
  57. package/dist/course/course.service.d.ts +2 -0
  58. package/dist/course/course.service.d.ts.map +1 -1
  59. package/dist/course/course.service.js +36 -2
  60. package/dist/course/course.service.js.map +1 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  62. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  63. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  64. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  65. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  66. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  67. package/dist/course/dto/create-course-export.dto.js +6 -0
  68. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  69. package/dist/course/ffmpeg.util.d.ts +10 -0
  70. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  71. package/dist/course/ffmpeg.util.js +79 -0
  72. package/dist/course/ffmpeg.util.js.map +1 -0
  73. package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  75. package/dist/course/lms-bulk-upload-automation.service.js +33 -16
  76. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  77. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  78. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  79. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  80. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  81. package/dist/course/lms-bulk-upload.service.js +48 -29
  82. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  83. package/dist/course/subtitle.util.d.ts +46 -0
  84. package/dist/course/subtitle.util.d.ts.map +1 -0
  85. package/dist/course/subtitle.util.js +206 -0
  86. package/dist/course/subtitle.util.js.map +1 -0
  87. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  88. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  89. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  90. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  91. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  92. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  93. package/dist/enterprise/training/training-student.service.js +197 -10
  94. package/dist/enterprise/training/training-student.service.js.map +1 -1
  95. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  96. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  97. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  98. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  99. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  100. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  101. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  102. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  103. package/dist/lms.module.d.ts.map +1 -1
  104. package/dist/lms.module.js +14 -0
  105. package/dist/lms.module.js.map +1 -1
  106. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  107. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  108. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  109. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  110. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  111. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  112. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  113. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  114. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  115. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  116. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  117. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  118. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  119. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  120. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  121. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  122. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  123. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  124. package/dist/platforma/platforma-performance.service.js +500 -0
  125. package/dist/platforma/platforma-performance.service.js.map +1 -0
  126. package/dist/platforma/platforma-search.service.d.ts +21 -0
  127. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  128. package/dist/platforma/platforma-search.service.js +64 -0
  129. package/dist/platforma/platforma-search.service.js.map +1 -0
  130. package/dist/platforma/platforma-video.service.d.ts +8 -0
  131. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  132. package/dist/platforma/platforma-video.service.js +45 -2
  133. package/dist/platforma/platforma-video.service.js.map +1 -1
  134. package/dist/platforma/platforma.controller.d.ts +213 -1
  135. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  136. package/dist/platforma/platforma.controller.js +159 -2
  137. package/dist/platforma/platforma.controller.js.map +1 -1
  138. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  139. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  140. package/dist/realtime/lms-realtime.controller.js +31 -0
  141. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  142. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  143. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  144. package/dist/realtime/lms-realtime.service.js.map +1 -1
  145. package/dist/training/dto/create-training.dto.d.ts +9 -0
  146. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  147. package/dist/training/dto/create-training.dto.js +45 -1
  148. package/dist/training/dto/create-training.dto.js.map +1 -1
  149. package/dist/training/training.controller.d.ts +144 -0
  150. package/dist/training/training.controller.d.ts.map +1 -1
  151. package/dist/training/training.service.d.ts +149 -0
  152. package/dist/training/training.service.d.ts.map +1 -1
  153. package/dist/training/training.service.js +332 -167
  154. package/dist/training/training.service.js.map +1 -1
  155. package/hedhog/data/image_type.yaml +10 -0
  156. package/hedhog/data/route.yaml +251 -0
  157. package/hedhog/data/setting_group.yaml +97 -0
  158. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  159. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  160. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  161. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  162. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  163. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  164. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  167. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
  168. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  169. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  170. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  171. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  172. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  173. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  174. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  175. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  176. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  177. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  178. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  179. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  180. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  181. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  182. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
  183. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  184. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  185. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  186. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  187. package/hedhog/frontend/app/courses/page.tsx.ejs +66 -13
  188. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  189. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  190. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  191. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  192. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  193. package/hedhog/frontend/app/paths/page.tsx.ejs +650 -168
  194. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  195. package/hedhog/frontend/messages/en.json +41 -12
  196. package/hedhog/frontend/messages/pt.json +44 -13
  197. package/hedhog/query/triggers.sql +33 -0
  198. package/hedhog/table/course_ai_usage.yaml +46 -0
  199. package/hedhog/table/course_enrollment.yaml +3 -0
  200. package/hedhog/table/course_lesson.yaml +3 -0
  201. package/hedhog/table/course_lesson_answer.yaml +37 -0
  202. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  203. package/hedhog/table/learning_path.yaml +6 -0
  204. package/hedhog/table/learning_path_module.yaml +22 -0
  205. package/hedhog/table/learning_path_step.yaml +9 -6
  206. package/hedhog/table/lesson_view_event.yaml +66 -0
  207. package/package.json +8 -7
  208. package/src/certificate/certificate.controller.ts +2 -0
  209. package/src/certificate/certificate.service.ts +99 -0
  210. package/src/course/course-ai-usage.service.ts +221 -0
  211. package/src/course/course-audio-transcription.service.ts +471 -43
  212. package/src/course/course-export-scorm12.service.ts +149 -5
  213. package/src/course/course-export.service.ts +1 -0
  214. package/src/course/course-lesson.controller.ts +59 -6
  215. package/src/course/course-structure.controller.ts +19 -1
  216. package/src/course/course-structure.service.ts +184 -10
  217. package/src/course/course-transcription-translation.service.ts +293 -0
  218. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  219. package/src/course/course-video-hls.service.ts +30 -10
  220. package/src/course/course.module.ts +9 -0
  221. package/src/course/course.service.ts +46 -1
  222. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  223. package/src/course/dto/create-course-export.dto.ts +6 -0
  224. package/src/course/ffmpeg.util.ts +65 -0
  225. package/src/course/lms-bulk-upload-automation.service.ts +33 -8
  226. package/src/course/lms-bulk-upload.service.ts +20 -1
  227. package/src/course/subtitle.util.ts +220 -0
  228. package/src/enterprise/training/training-student.service.ts +224 -4
  229. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  230. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  231. package/src/lms.module.ts +14 -0
  232. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  233. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  234. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  235. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  236. package/src/platforma/platforma-performance.service.ts +606 -0
  237. package/src/platforma/platforma-search.service.ts +48 -0
  238. package/src/platforma/platforma-video.service.ts +59 -3
  239. package/src/platforma/platforma.controller.ts +130 -0
  240. package/src/realtime/lms-realtime.controller.ts +27 -1
  241. package/src/realtime/lms-realtime.service.ts +2 -1
  242. package/src/training/dto/create-training.dto.ts +36 -0
  243. package/src/training/training.service.ts +360 -163
@@ -1,7 +1,7 @@
1
1
  import { PrismaService } from '@hed-hog/api-prisma';
2
2
  import { IntegrationDeveloperApiService } from '@hed-hog/core';
3
3
  import { Inject, Injectable, forwardRef } from '@nestjs/common';
4
- import { CreateTrainingDto, LearningPathItemDto } from './dto/create-training.dto';
4
+ import { CreateTrainingDto, LearningPathItemDto, LearningPathModuleDto } from './dto/create-training.dto';
5
5
  import { UpdateTrainingDto } from './dto/update-training.dto';
6
6
 
7
7
  @Injectable()
@@ -63,6 +63,35 @@ export class TrainingService {
63
63
  return extrasById;
64
64
  }
65
65
 
66
+ private async getTrainingCertificateTemplate(id: number): Promise<{
67
+ id: number;
68
+ name: string;
69
+ template_content: string;
70
+ status: string;
71
+ } | null> {
72
+ try {
73
+ const rows = (await this.prisma.$queryRawUnsafe(
74
+ `
75
+ SELECT ct.id, ct.name, ct.template_content, ct.status
76
+ FROM learning_path lp
77
+ JOIN certificate_template ct ON ct.id = lp.certificate_template_id
78
+ WHERE lp.id = $1
79
+ LIMIT 1
80
+ `,
81
+ id,
82
+ )) as Array<{
83
+ id: number;
84
+ name: string;
85
+ template_content: string;
86
+ status: string;
87
+ }>;
88
+
89
+ return rows[0] ?? null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
66
95
  private async persistTrainingProgressMode(
67
96
  id: number,
68
97
  progressMode?: 'sequential' | 'free',
@@ -86,6 +115,29 @@ export class TrainingService {
86
115
  }
87
116
  }
88
117
 
118
+ private async persistTrainingCertificateTemplateId(
119
+ id: number,
120
+ certificateTemplateId?: number | null,
121
+ ) {
122
+ if (certificateTemplateId === undefined) {
123
+ return;
124
+ }
125
+
126
+ try {
127
+ await this.prisma.$executeRawUnsafe(
128
+ `
129
+ UPDATE learning_path
130
+ SET certificate_template_id = $1
131
+ WHERE id = $2
132
+ `,
133
+ certificateTemplateId ?? null,
134
+ id,
135
+ );
136
+ } catch {
137
+ // Some environments may still be behind the current learning_path schema.
138
+ }
139
+ }
140
+
89
141
  async list(params: {
90
142
  page?: number;
91
143
  pageSize?: number;
@@ -147,32 +199,43 @@ export class TrainingService {
147
199
  where,
148
200
  orderBy: { created_at: 'desc' },
149
201
  include: {
150
- learning_path_step: {
202
+ learning_path_module: {
151
203
  orderBy: { order: 'asc' },
152
204
  include: {
153
- course: {
154
- select: {
155
- id: true,
156
- title: true,
157
- duration_hours: true,
158
- course_category: {
159
- include: {
160
- category: {
161
- select: { slug: true },
205
+ learning_path_step: {
206
+ orderBy: { order: 'asc' },
207
+ include: {
208
+ course: {
209
+ select: {
210
+ id: true,
211
+ title: true,
212
+ duration_hours: true,
213
+ course_category: {
214
+ include: {
215
+ category: {
216
+ select: { slug: true },
217
+ },
218
+ },
162
219
  },
163
220
  },
164
221
  },
165
- },
166
- },
167
- exam: {
168
- select: {
169
- id: true,
170
- title: true,
171
- time_limit_minutes: true,
222
+ exam: {
223
+ select: {
224
+ id: true,
225
+ title: true,
226
+ time_limit_minutes: true,
227
+ },
228
+ },
172
229
  },
173
230
  },
174
231
  },
175
232
  },
233
+ learning_path_image: {
234
+ where: { image_type: { slug: 'learning-path-banner' } },
235
+ include: { file: { select: { id: true } } },
236
+ orderBy: [{ is_primary: 'desc' as const }, { order: 'asc' as const }],
237
+ take: 1,
238
+ },
176
239
  _count: {
177
240
  select: {
178
241
  learning_path_enrollment: true,
@@ -216,35 +279,54 @@ export class TrainingService {
216
279
  }
217
280
 
218
281
  async getById(id: number) {
219
- const path = await this.prisma.learning_path.findUnique({
220
- where: { id },
282
+ const stepInclude = {
283
+ orderBy: { order: 'asc' as const },
221
284
  include: {
222
- learning_path_step: {
223
- orderBy: { order: 'asc' },
224
- include: {
225
- course: {
226
- select: {
227
- id: true,
228
- title: true,
229
- duration_hours: true,
230
- course_category: {
231
- include: {
232
- category: {
233
- select: { slug: true },
234
- },
235
- },
285
+ course: {
286
+ select: {
287
+ id: true,
288
+ title: true,
289
+ duration_hours: true,
290
+ course_category: {
291
+ include: {
292
+ category: {
293
+ select: { slug: true },
236
294
  },
237
295
  },
238
296
  },
239
- exam: {
240
- select: {
241
- id: true,
242
- title: true,
243
- time_limit_minutes: true,
244
- },
297
+ course_image: {
298
+ where: { image_type: { slug: 'course-logo' } },
299
+ select: { file: { select: { id: true } } },
300
+ orderBy: { order: 'asc' as const },
301
+ take: 1,
245
302
  },
246
303
  },
247
304
  },
305
+ exam: {
306
+ select: {
307
+ id: true,
308
+ title: true,
309
+ time_limit_minutes: true,
310
+ },
311
+ },
312
+ },
313
+ };
314
+
315
+ const path = await this.prisma.learning_path.findUnique({
316
+ where: { id },
317
+ include: {
318
+ learning_path_module: {
319
+ orderBy: { order: 'asc' },
320
+ include: {
321
+ learning_path_step: stepInclude,
322
+ },
323
+ },
324
+ learning_path_image: {
325
+ where: { image_type: { slug: 'learning-path-banner' } },
326
+ include: { file: { select: { id: true } } },
327
+ orderBy: [{ is_primary: 'desc' as const }, { order: 'asc' as const }],
328
+ take: 1,
329
+ },
248
330
  _count: {
249
331
  select: {
250
332
  learning_path_enrollment: true,
@@ -256,13 +338,17 @@ export class TrainingService {
256
338
 
257
339
  if (!path) return null;
258
340
 
259
- const extrasById = await this.getTrainingExtras([id]);
260
- return this.mapTraining(path, extrasById.get(id));
341
+ const [extrasById, certificateTemplate] = await Promise.all([
342
+ this.getTrainingExtras([id]),
343
+ this.getTrainingCertificateTemplate(id),
344
+ ]);
345
+ return this.mapTraining(path, extrasById.get(id), certificateTemplate);
261
346
  }
262
347
 
263
348
  async create(dto: CreateTrainingDto) {
264
349
  const slug = dto.slug?.trim() || this.slugify(dto.title);
265
- const stepItems = this.resolveIncomingSteps(dto);
350
+ const hasModules = (dto.modules?.length ?? 0) > 0;
351
+ const stepItems = hasModules ? [] : this.resolveIncomingSteps(dto);
266
352
  const normalizedLevel = this.normalizeLevel(dto.level) ?? 'beginner';
267
353
  const normalizedStatus = this.normalizeStatus(dto.status) ?? 'draft';
268
354
  const normalizedProgressMode =
@@ -278,41 +364,8 @@ export class TrainingService {
278
364
  status: normalizedStatus,
279
365
  ...(dto.primaryColor !== undefined && { primary_color: dto.primaryColor }),
280
366
  ...(dto.secondaryColor !== undefined && { secondary_color: dto.secondaryColor }),
281
- ...(stepItems.length > 0 && {
282
- learning_path_step: {
283
- create: stepItems.map((item, index) =>
284
- this.toLearningPathCreateStep(item, index),
285
- ),
286
- },
287
- }),
288
367
  },
289
368
  include: {
290
- learning_path_step: {
291
- orderBy: { order: 'asc' },
292
- include: {
293
- course: {
294
- select: {
295
- id: true,
296
- title: true,
297
- duration_hours: true,
298
- course_category: {
299
- include: {
300
- category: {
301
- select: { slug: true },
302
- },
303
- },
304
- },
305
- },
306
- },
307
- exam: {
308
- select: {
309
- id: true,
310
- title: true,
311
- time_limit_minutes: true,
312
- },
313
- },
314
- },
315
- },
316
369
  _count: {
317
370
  select: {
318
371
  learning_path_enrollment: true,
@@ -322,14 +375,48 @@ export class TrainingService {
322
375
  },
323
376
  });
324
377
 
378
+ if (hasModules) {
379
+ const resolvedModules = this.resolveIncomingModules(dto.modules!);
380
+ for (const mod of resolvedModules) {
381
+ await this.prisma.learning_path_module.create({
382
+ data: {
383
+ learning_path_id: created.id,
384
+ title: mod.title,
385
+ description: mod.description,
386
+ order: mod.order,
387
+ learning_path_step: {
388
+ create: mod.items.map((item) => ({
389
+ ...this.toLearningPathCreateStep(item, item.order),
390
+ learning_path_id: created.id,
391
+ })),
392
+ },
393
+ },
394
+ });
395
+ }
396
+ } else if (stepItems.length > 0) {
397
+ await this.prisma.learning_path_module.create({
398
+ data: {
399
+ learning_path_id: created.id,
400
+ title: '',
401
+ order: 0,
402
+ learning_path_step: {
403
+ create: stepItems.map((item, index) => ({
404
+ ...this.toLearningPathCreateStep(item, index),
405
+ learning_path_id: created.id,
406
+ })),
407
+ },
408
+ },
409
+ });
410
+ }
411
+
325
412
  await this.persistTrainingProgressMode(created.id, normalizedProgressMode);
413
+ await this.persistTrainingCertificateTemplateId(created.id, dto.certificateTemplateId);
326
414
 
327
- const extrasById = await this.getTrainingExtras([created.id]);
415
+ if (dto.bannerFileId !== undefined) {
416
+ await this.syncLearningPathImage(created.id, dto.bannerFileId);
417
+ }
328
418
 
329
- const trainingResult = this.mapTraining(
330
- created,
331
- extrasById.get(created.id),
332
- );
419
+ const result = await this.getById(created.id);
333
420
 
334
421
  await this.integrationApi.publishEvent({
335
422
  eventName: 'lms.training.created',
@@ -339,12 +426,13 @@ export class TrainingService {
339
426
  payload: { id: created.id, title: dto.title, slug, status: normalizedStatus },
340
427
  }).catch(() => null);
341
428
 
342
- return trainingResult;
429
+ return result;
343
430
  }
344
431
 
345
432
  async update(id: number, dto: UpdateTrainingDto) {
346
- const stepItems = this.resolveIncomingSteps(dto);
347
- const stepsWereProvided = dto.items !== undefined || dto.courseIds !== undefined;
433
+ const modulesWereProvided = dto.modules !== undefined;
434
+ const stepsWereProvided = !modulesWereProvided && (dto.items !== undefined || dto.courseIds !== undefined);
435
+ const stepItems = stepsWereProvided ? this.resolveIncomingSteps(dto) : [];
348
436
  const normalizedLevel =
349
437
  dto.level !== undefined ? this.normalizeLevel(dto.level) : undefined;
350
438
  const normalizedStatus =
@@ -354,15 +442,18 @@ export class TrainingService {
354
442
  ? this.normalizeProgressMode(dto.progressMode)
355
443
  : undefined;
356
444
 
357
- if (stepsWereProvided) {
445
+ if (modulesWereProvided) {
446
+ // Delete all modules — CASCADE removes their steps automatically
447
+ await this.prisma.learning_path_module.deleteMany({
448
+ where: { learning_path_id: id },
449
+ });
450
+ } else if (stepsWereProvided) {
358
451
  await this.prisma.learning_path_step.deleteMany({
359
- where: {
360
- learning_path_id: id,
361
- },
452
+ where: { learning_path_id: id },
362
453
  });
363
454
  }
364
455
 
365
- const updated = await this.prisma.learning_path.update({
456
+ await this.prisma.learning_path.update({
366
457
  where: { id },
367
458
  data: {
368
459
  ...(dto.title !== undefined && { title: dto.title }),
@@ -375,56 +466,53 @@ export class TrainingService {
375
466
  ...(normalizedStatus !== undefined && { status: normalizedStatus }),
376
467
  ...(dto.primaryColor !== undefined && { primary_color: dto.primaryColor }),
377
468
  ...(dto.secondaryColor !== undefined && { secondary_color: dto.secondaryColor }),
378
- ...(stepsWereProvided &&
379
- stepItems.length > 0 && {
380
- learning_path_step: {
381
- create: stepItems.map((item, index) =>
382
- this.toLearningPathCreateStep(item, index),
383
- ),
384
- },
385
- }),
386
469
  },
387
- include: {
388
- learning_path_step: {
389
- orderBy: { order: 'asc' },
390
- include: {
391
- course: {
392
- select: {
393
- id: true,
394
- title: true,
395
- duration_hours: true,
396
- course_category: {
397
- include: {
398
- category: {
399
- select: { slug: true },
400
- },
401
- },
402
- },
403
- },
404
- },
405
- exam: {
406
- select: {
407
- id: true,
408
- title: true,
409
- time_limit_minutes: true,
410
- },
470
+ });
471
+
472
+ if (modulesWereProvided) {
473
+ const resolvedModules = this.resolveIncomingModules(dto.modules!);
474
+ for (const mod of resolvedModules) {
475
+ await this.prisma.learning_path_module.create({
476
+ data: {
477
+ learning_path_id: id,
478
+ title: mod.title,
479
+ description: mod.description,
480
+ order: mod.order,
481
+ learning_path_step: {
482
+ create: mod.items.map((item) => ({
483
+ ...this.toLearningPathCreateStep(item, item.order),
484
+ learning_path_id: id,
485
+ })),
411
486
  },
412
487
  },
413
- },
414
- _count: {
415
- select: {
416
- learning_path_enrollment: true,
417
- learning_path_step: true,
488
+ });
489
+ }
490
+ } else if (stepsWereProvided && stepItems.length > 0) {
491
+ await this.prisma.learning_path_module.create({
492
+ data: {
493
+ learning_path_id: id,
494
+ title: '',
495
+ order: 0,
496
+ learning_path_step: {
497
+ create: stepItems.map((item, index) => ({
498
+ ...this.toLearningPathCreateStep(item, index),
499
+ learning_path_id: id,
500
+ })),
418
501
  },
419
502
  },
420
- },
421
- });
503
+ });
504
+ }
422
505
 
423
506
  await this.persistTrainingProgressMode(id, normalizedProgressMode);
507
+ if (dto.certificateTemplateId !== undefined) {
508
+ await this.persistTrainingCertificateTemplateId(id, dto.certificateTemplateId);
509
+ }
424
510
 
425
- const extrasById = await this.getTrainingExtras([id]);
511
+ if (dto.bannerFileId !== undefined) {
512
+ await this.syncLearningPathImage(id, dto.bannerFileId);
513
+ }
426
514
 
427
- const updateTrainingResult = this.mapTraining(updated, extrasById.get(id));
515
+ const updateTrainingResult = await this.getById(id);
428
516
 
429
517
  await this.integrationApi.publishEvent({
430
518
  eventName: 'lms.training.updated',
@@ -451,15 +539,53 @@ export class TrainingService {
451
539
  return { success: true };
452
540
  }
453
541
 
542
+ private mapStep(step: any, index: number) {
543
+ if (step?.course) {
544
+ const logoFileId = step.course.course_image?.[0]?.file?.id ?? null;
545
+ return {
546
+ id: step.id,
547
+ type: 'course' as const,
548
+ itemId: step.course.id,
549
+ title: step.course.title,
550
+ durationHours: step.course.duration_hours ?? 0,
551
+ order: step.order ?? index,
552
+ isRequired: step.is_required !== false,
553
+ logoImage: logoFileId ? `/file/open/${logoFileId}` : null,
554
+ };
555
+ }
556
+ if (step?.exam) {
557
+ return {
558
+ id: step.id,
559
+ type: 'exam' as const,
560
+ itemId: step.exam.id,
561
+ title: step.exam.title,
562
+ durationMinutes: step.exam.time_limit_minutes ?? 0,
563
+ order: step.order ?? index,
564
+ isRequired: step.is_required !== false,
565
+ logoImage: null,
566
+ };
567
+ }
568
+ return null;
569
+ }
570
+
454
571
  private mapTraining(
455
572
  path: any,
456
573
  extras?: { progress_mode?: 'sequential' | 'free' | null },
574
+ certificateTemplate?: { id: number; name: string; template_content: string; status: string } | null,
457
575
  ) {
458
- const steps = [...(path.learning_path_step ?? [])].sort(
576
+ const modulesRaw = [...(path.learning_path_module ?? [])].sort(
459
577
  (a: any, b: any) => (a?.order ?? 0) - (b?.order ?? 0),
460
578
  );
461
- const courseSteps = steps.filter((step: any) => step?.course);
462
- const examSteps = steps.filter((step: any) => step?.exam);
579
+
580
+ // All steps across all modules (flattened, for backward-compat fields)
581
+ const allSteps = modulesRaw.flatMap((mod: any) =>
582
+ [...(mod.learning_path_step ?? [])].sort(
583
+ (a: any, b: any) => (a?.order ?? 0) - (b?.order ?? 0),
584
+ ),
585
+ );
586
+
587
+ const courseSteps = allSteps.filter((step: any) => step?.course);
588
+ const examSteps = allSteps.filter((step: any) => step?.exam);
463
589
 
464
590
  const courses = courseSteps.map((step: any) => step.course.title);
465
591
  const exams = examSteps.map((step: any) => step.exam.title);
@@ -470,40 +596,25 @@ export class TrainingService {
470
596
  0,
471
597
  );
472
598
 
473
- const firstCategorySlug = steps
599
+ const firstCategorySlug = allSteps
474
600
  .flatMap((step: any) => step.course?.course_category ?? [])
475
601
  .map((item: any) => item.category?.slug)
476
602
  .find(Boolean);
477
603
 
478
- const items = steps
479
- .map((step: any, index: number) => {
480
- if (step?.course) {
481
- return {
482
- id: step.id,
483
- type: 'course' as const,
484
- itemId: step.course.id,
485
- title: step.course.title,
486
- durationHours: step.course.duration_hours ?? 0,
487
- order: step.order ?? index,
488
- isRequired: step.is_required !== false,
489
- };
490
- }
491
-
492
- if (step?.exam) {
493
- return {
494
- id: step.id,
495
- type: 'exam' as const,
496
- itemId: step.exam.id,
497
- title: step.exam.title,
498
- durationMinutes: step.exam.time_limit_minutes ?? 0,
499
- order: step.order ?? index,
500
- isRequired: step.is_required !== false,
501
- };
502
- }
503
-
504
- return null;
505
- })
506
- .filter(Boolean);
604
+ const items = allSteps.map(this.mapStep.bind(this)).filter(Boolean);
605
+
606
+ const modules = modulesRaw.map((mod: any, modIndex: number) => ({
607
+ id: mod.id,
608
+ title: mod.title,
609
+ description: mod.description ?? null,
610
+ order: mod.order ?? modIndex,
611
+ items: [...(mod.learning_path_step ?? [])]
612
+ .sort((a: any, b: any) => (a?.order ?? 0) - (b?.order ?? 0))
613
+ .map(this.mapStep.bind(this))
614
+ .filter(Boolean),
615
+ }));
616
+
617
+ const bannerImage = (path.learning_path_image ?? [])[0];
507
618
 
508
619
  return {
509
620
  id: path.id,
@@ -517,6 +628,7 @@ export class TrainingService {
517
628
  courseIds,
518
629
  examIds,
519
630
  items,
631
+ modules,
520
632
  progressionMode: this.progressModeToPt(extras?.progress_mode),
521
633
  cargaTotal,
522
634
  alunos: path._count?.learning_path_enrollment ?? 0,
@@ -524,12 +636,21 @@ export class TrainingService {
524
636
  criadoEm: path.created_at,
525
637
  primaryColor: path.primary_color ?? undefined,
526
638
  secondaryColor: path.secondary_color ?? undefined,
639
+ bannerFileId: bannerImage?.file?.id ?? null,
527
640
  code: this.idToCode(path.id),
528
641
  slug: path.slug,
529
642
  level: path.level,
530
643
  statusRaw: path.status,
531
644
  students: path._count?.learning_path_enrollment ?? 0,
532
645
  createdAt: path.created_at,
646
+ certificateTemplate: certificateTemplate
647
+ ? {
648
+ id: certificateTemplate.id,
649
+ name: certificateTemplate.name,
650
+ templateContent: certificateTemplate.template_content,
651
+ status: certificateTemplate.status,
652
+ }
653
+ : null,
533
654
  };
534
655
  }
535
656
 
@@ -574,6 +695,27 @@ export class TrainingService {
574
695
  }));
575
696
  }
576
697
 
698
+ private resolveIncomingModules(modules: LearningPathModuleDto[]) {
699
+ return modules.map((mod, i) => ({
700
+ title: mod.title,
701
+ description: mod.description,
702
+ order: Number.isFinite(mod.order) ? (mod.order as number) : i,
703
+ items: (mod.items ?? [])
704
+ .map((item, j) => ({
705
+ type: item.type,
706
+ itemId: Number(item.itemId),
707
+ order: Number.isFinite(item.order) ? (item.order as number) : j,
708
+ isRequired: item.isRequired !== false,
709
+ }))
710
+ .filter(
711
+ (item) =>
712
+ (item.type === 'course' || item.type === 'exam') &&
713
+ Number.isFinite(item.itemId) &&
714
+ item.itemId > 0,
715
+ ),
716
+ }));
717
+ }
718
+
577
719
  private toLearningPathCreateStep(
578
720
  item: {
579
721
  type: 'course' | 'exam';
@@ -704,4 +846,59 @@ export class TrainingService {
704
846
  private idToCode(id: number) {
705
847
  return `TR-${String(id).padStart(3, '0')}`;
706
848
  }
849
+
850
+ private async syncLearningPathImage(pathId: number, fileId: number | null) {
851
+ const imageType = await this.prisma.image_type.findFirst({
852
+ where: { slug: 'learning-path-banner', applies_to_learning_path: true },
853
+ select: { id: true },
854
+ });
855
+
856
+ if (!imageType) return;
857
+
858
+ if (fileId === null) {
859
+ await this.prisma.learning_path_image.deleteMany({
860
+ where: { learning_path_id: pathId, image_type_id: imageType.id },
861
+ });
862
+ return;
863
+ }
864
+
865
+ const normalizedFileId = Number(fileId);
866
+ if (!Number.isInteger(normalizedFileId) || normalizedFileId <= 0) return;
867
+
868
+ const file = await this.prisma.file.findUnique({
869
+ where: { id: normalizedFileId },
870
+ select: { id: true },
871
+ });
872
+ if (!file) return;
873
+
874
+ const existing = await this.prisma.learning_path_image.findMany({
875
+ where: { learning_path_id: pathId, image_type_id: imageType.id },
876
+ orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
877
+ select: { id: true },
878
+ });
879
+
880
+ if (existing.length > 0) {
881
+ const [primary, ...duplicates] = existing;
882
+ await this.prisma.learning_path_image.update({
883
+ where: { id: primary.id },
884
+ data: { file_id: normalizedFileId, is_primary: true },
885
+ });
886
+ if (duplicates.length > 0) {
887
+ await this.prisma.learning_path_image.deleteMany({
888
+ where: { id: { in: duplicates.map((r) => r.id) } },
889
+ });
890
+ }
891
+ return;
892
+ }
893
+
894
+ await this.prisma.learning_path_image.create({
895
+ data: {
896
+ learning_path_id: pathId,
897
+ file_id: normalizedFileId,
898
+ image_type_id: imageType.id,
899
+ order: 0,
900
+ is_primary: true,
901
+ },
902
+ });
903
+ }
707
904
  }