@hed-hog/lms 0.0.366 → 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.
- package/dist/certificate/certificate.controller.d.ts +1 -1
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +4 -2
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +50 -0
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +73 -0
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/course/course-ai-usage.service.d.ts +58 -0
- package/dist/course/course-ai-usage.service.d.ts.map +1 -0
- package/dist/course/course-ai-usage.service.js +176 -0
- package/dist/course/course-ai-usage.service.js.map +1 -0
- package/dist/course/course-audio-transcription.service.d.ts +65 -1
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +381 -29
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12.service.d.ts +3 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
- package/dist/course/course-export-scorm12.service.js +141 -6
- package/dist/course/course-export-scorm12.service.js.map +1 -1
- package/dist/course/course-export.service.d.ts.map +1 -1
- package/dist/course/course-export.service.js +2 -1
- package/dist/course/course-export.service.js.map +1 -1
- package/dist/course/course-lesson.controller.d.ts +25 -3
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +71 -8
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +26 -5
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +31 -1
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +37 -5
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +165 -20
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-transcription-translation.service.d.ts +31 -0
- package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
- package/dist/course/course-transcription-translation.service.js +227 -0
- package/dist/course/course-transcription-translation.service.js.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +7 -7
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
- package/dist/course/dto/create-course-export.dto.d.ts +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-export.dto.js +6 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +26 -13
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +3 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +48 -29
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/subtitle.util.d.ts +46 -0
- package/dist/course/subtitle.util.d.ts.map +1 -0
- package/dist/course/subtitle.util.js +206 -0
- package/dist/course/subtitle.util.js.map +1 -0
- package/dist/enterprise/training/training-student.service.d.ts +27 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +197 -10
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +4 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma-performance.service.js +121 -121
- package/dist/platforma/platforma-video.service.d.ts +8 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -1
- package/dist/platforma/platforma-video.service.js +45 -2
- package/dist/platforma/platforma-video.service.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +99 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +111 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/training/dto/create-training.dto.d.ts +9 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -1
- package/dist/training/dto/create-training.dto.js +45 -1
- package/dist/training/dto/create-training.dto.js.map +1 -1
- package/dist/training/training.controller.d.ts +144 -0
- package/dist/training/training.controller.d.ts.map +1 -1
- package/dist/training/training.service.d.ts +149 -0
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +332 -167
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/data/image_type.yaml +10 -0
- package/hedhog/data/route.yaml +251 -0
- package/hedhog/data/setting_group.yaml +97 -0
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
- package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
- package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
- package/hedhog/frontend/app/courses/page.tsx.ejs +26 -4
- package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
- package/hedhog/frontend/messages/en.json +23 -12
- package/hedhog/frontend/messages/pt.json +23 -12
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -0
- package/hedhog/table/course_lesson.yaml +3 -0
- package/hedhog/table/course_lesson_answer.yaml +37 -0
- package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
- package/hedhog/table/learning_path.yaml +6 -0
- package/hedhog/table/learning_path_module.yaml +22 -0
- package/hedhog/table/learning_path_step.yaml +9 -6
- package/hedhog/table/lesson_view_event.yaml +66 -66
- package/package.json +9 -9
- package/src/certificate/certificate.controller.ts +2 -0
- package/src/certificate/certificate.service.ts +99 -0
- package/src/course/course-ai-usage.service.ts +221 -0
- package/src/course/course-audio-transcription.service.ts +471 -43
- package/src/course/course-export-scorm12.service.ts +149 -5
- package/src/course/course-export.service.ts +1 -0
- package/src/course/course-lesson.controller.ts +59 -6
- package/src/course/course-structure.controller.ts +16 -0
- package/src/course/course-structure.service.ts +184 -10
- package/src/course/course-transcription-translation.service.ts +293 -0
- package/src/course/course-video-agent-pipeline.service.ts +471 -471
- package/src/course/course.module.ts +4 -0
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +6 -0
- package/src/course/ffmpeg.util.ts +65 -65
- package/src/course/lms-bulk-upload-automation.service.ts +29 -7
- package/src/course/lms-bulk-upload.service.ts +20 -1
- package/src/course/subtitle.util.ts +220 -0
- package/src/enterprise/training/training-student.service.ts +224 -4
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
- package/src/lms.module.ts +4 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -30
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
- package/src/platforma/platforma-heartbeat.service.ts +33 -33
- package/src/platforma/platforma-performance.service.ts +606 -606
- package/src/platforma/platforma-search.service.ts +48 -48
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +88 -0
- package/src/training/dto/create-training.dto.ts +36 -0
- 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
|
-
|
|
202
|
+
learning_path_module: {
|
|
151
203
|
orderBy: { order: 'asc' },
|
|
152
204
|
include: {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
220
|
-
|
|
282
|
+
const stepInclude = {
|
|
283
|
+
orderBy: { order: 'asc' as const },
|
|
221
284
|
include: {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
260
|
-
|
|
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
|
|
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
|
-
|
|
415
|
+
if (dto.bannerFileId !== undefined) {
|
|
416
|
+
await this.syncLearningPathImage(created.id, dto.bannerFileId);
|
|
417
|
+
}
|
|
328
418
|
|
|
329
|
-
const
|
|
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
|
|
429
|
+
return result;
|
|
343
430
|
}
|
|
344
431
|
|
|
345
432
|
async update(id: number, dto: UpdateTrainingDto) {
|
|
346
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
511
|
+
if (dto.bannerFileId !== undefined) {
|
|
512
|
+
await this.syncLearningPathImage(id, dto.bannerFileId);
|
|
513
|
+
}
|
|
426
514
|
|
|
427
|
-
const updateTrainingResult = this.
|
|
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
|
|
576
|
+
const modulesRaw = [...(path.learning_path_module ?? [])].sort(
|
|
459
577
|
(a: any, b: any) => (a?.order ?? 0) - (b?.order ?? 0),
|
|
460
578
|
);
|
|
461
|
-
|
|
462
|
-
|
|
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 =
|
|
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 =
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
}
|