@hed-hog/lms 0.0.338 → 0.0.347
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/class-group/class-group.controller.d.ts +11 -11
- package/dist/class-group/class-group.service.d.ts +11 -11
- package/dist/course/course.controller.d.ts +6 -1
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +19 -2
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.service.d.ts +6 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +51 -8
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -0
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +5 -0
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +12 -12
- package/dist/enterprise/enterprise.service.d.ts +12 -12
- package/dist/enterprise/training/training-admin.controller.d.ts +7 -7
- package/dist/enterprise/training/training-admin.service.d.ts +7 -7
- package/dist/evaluation/evaluation.controller.d.ts +6 -6
- package/dist/evaluation/evaluation.service.d.ts +6 -6
- package/dist/instructor/instructor.controller.d.ts +1 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.service.d.ts +2 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +9 -7
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/training/dto/create-training.dto.d.ts +1 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -1
- package/dist/training/dto/create-training.dto.js +5 -0
- package/dist/training/dto/create-training.dto.js.map +1 -1
- package/dist/training/training.controller.d.ts +4 -0
- package/dist/training/training.controller.d.ts.map +1 -1
- package/dist/training/training.service.d.ts +8 -0
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +71 -6
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +38 -9
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +33 -6
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
- package/hedhog/frontend/app/classes/page.tsx.ejs +28 -6
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1 -1
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +25 -18
- package/hedhog/frontend/app/paths/page.tsx.ejs +68 -5
- package/hedhog/frontend/app/training/page.tsx.ejs +70 -6
- package/hedhog/frontend/messages/pt.json +14 -1
- package/hedhog/table/learning_path.yaml +4 -0
- package/package.json +6 -6
- package/src/course/course.controller.ts +18 -0
- package/src/course/course.service.ts +73 -2
- package/src/course/dto/create-course.dto.ts +4 -0
- package/src/instructor/instructor.service.ts +2 -0
- package/src/training/dto/create-training.dto.ts +4 -0
- package/src/training/training.service.ts +104 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/lms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.347",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api-prisma": "0.0.6",
|
|
13
13
|
"@hed-hog/api-types": "0.0.1",
|
|
14
|
+
"@hed-hog/api-locale": "0.0.14",
|
|
14
15
|
"@hed-hog/api": "0.0.8",
|
|
15
16
|
"@hed-hog/api-pagination": "0.0.7",
|
|
16
|
-
"@hed-hog/
|
|
17
|
-
"@hed-hog/contact": "0.0.
|
|
18
|
-
"@hed-hog/
|
|
19
|
-
"@hed-hog/core": "0.0.
|
|
20
|
-
"@hed-hog/finance": "0.0.338"
|
|
17
|
+
"@hed-hog/category": "0.0.347",
|
|
18
|
+
"@hed-hog/contact": "0.0.347",
|
|
19
|
+
"@hed-hog/finance": "0.0.347",
|
|
20
|
+
"@hed-hog/core": "0.0.347"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
@@ -28,6 +28,7 @@ export class CourseController {
|
|
|
28
28
|
@Query('status') status?: string,
|
|
29
29
|
@Query('level') level?: string,
|
|
30
30
|
@Query('category') category?: string,
|
|
31
|
+
@Query('offeringTypes') offeringTypes?: string | string[],
|
|
31
32
|
) {
|
|
32
33
|
return this.courseService.list({
|
|
33
34
|
page: page ? Number(page) : 1,
|
|
@@ -36,9 +37,26 @@ export class CourseController {
|
|
|
36
37
|
status,
|
|
37
38
|
level,
|
|
38
39
|
category,
|
|
40
|
+
offeringTypes: this.toArray(offeringTypes),
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
private toArray(value?: string | string[]) {
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value
|
|
47
|
+
.flatMap((item) => String(item).split(','))
|
|
48
|
+
.map((item) => item.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'string') {
|
|
52
|
+
return value
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((item) => item.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
@Get('stats')
|
|
43
61
|
stats() {
|
|
44
62
|
return this.courseService.stats();
|
|
@@ -94,6 +94,35 @@ export class CourseService {
|
|
|
94
94
|
return this.normalizeOptionalText(title) ?? slug;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
private async resolveCertificateTemplateId(value?: string | null) {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const normalized = String(value ?? '').trim();
|
|
103
|
+
|
|
104
|
+
if (!normalized) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const numericId = Number(normalized);
|
|
109
|
+
const template = await this.prisma.certificate_template.findFirst({
|
|
110
|
+
where:
|
|
111
|
+
Number.isInteger(numericId) && numericId > 0
|
|
112
|
+
? {
|
|
113
|
+
OR: [{ id: numericId }, { slug: normalized }],
|
|
114
|
+
}
|
|
115
|
+
: { slug: normalized },
|
|
116
|
+
select: { id: true },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!template) {
|
|
120
|
+
throw new BadRequestException('Certificate template not found');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return template.id;
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
async list(params: {
|
|
98
127
|
page?: number;
|
|
99
128
|
pageSize?: number;
|
|
@@ -101,6 +130,7 @@ export class CourseService {
|
|
|
101
130
|
status?: string;
|
|
102
131
|
level?: string;
|
|
103
132
|
category?: string;
|
|
133
|
+
offeringTypes?: string[];
|
|
104
134
|
}) {
|
|
105
135
|
const page = Math.max(Number(params.page) || 1, 1);
|
|
106
136
|
const pageSize = Math.max(Number(params.pageSize) || 12, 1);
|
|
@@ -128,6 +158,9 @@ export class CourseService {
|
|
|
128
158
|
some: { category: { slug: params.category } },
|
|
129
159
|
};
|
|
130
160
|
}
|
|
161
|
+
if (params.offeringTypes && params.offeringTypes.length > 0) {
|
|
162
|
+
where.offering_type = { in: params.offeringTypes };
|
|
163
|
+
}
|
|
131
164
|
|
|
132
165
|
const [courses, total] = await Promise.all([
|
|
133
166
|
this.prisma.course.findMany({
|
|
@@ -244,6 +277,9 @@ export class CourseService {
|
|
|
244
277
|
},
|
|
245
278
|
},
|
|
246
279
|
},
|
|
280
|
+
certificate_template: {
|
|
281
|
+
select: { id: true, slug: true },
|
|
282
|
+
},
|
|
247
283
|
_count: {
|
|
248
284
|
select: {
|
|
249
285
|
course_enrollment: true,
|
|
@@ -318,9 +354,18 @@ export class CourseService {
|
|
|
318
354
|
}
|
|
319
355
|
|
|
320
356
|
async create(dto: CreateCourseDto) {
|
|
321
|
-
const {
|
|
357
|
+
const {
|
|
358
|
+
categorySlugs,
|
|
359
|
+
logoFileId,
|
|
360
|
+
bannerFileId,
|
|
361
|
+
instructorIds,
|
|
362
|
+
certificateModel,
|
|
363
|
+
...data
|
|
364
|
+
} = dto;
|
|
322
365
|
const normalizedSlug = data.slug.trim();
|
|
323
366
|
const resolvedTitle = this.resolveCourseTitle(data.title, normalizedSlug);
|
|
367
|
+
const certificateTemplateId =
|
|
368
|
+
await this.resolveCertificateTemplateId(certificateModel);
|
|
324
369
|
|
|
325
370
|
const categories = categorySlugs?.length
|
|
326
371
|
? await this.prisma.category.findMany({
|
|
@@ -365,6 +410,9 @@ export class CourseService {
|
|
|
365
410
|
...(data.certificateWorkload !== undefined && {
|
|
366
411
|
certificate_workload: data.certificateWorkload,
|
|
367
412
|
}),
|
|
413
|
+
...(certificateTemplateId !== undefined && {
|
|
414
|
+
certificate_template_id: certificateTemplateId,
|
|
415
|
+
}),
|
|
368
416
|
...(data.primaryColor !== undefined && {
|
|
369
417
|
primary_color: data.primaryColor,
|
|
370
418
|
}),
|
|
@@ -427,6 +475,9 @@ export class CourseService {
|
|
|
427
475
|
},
|
|
428
476
|
},
|
|
429
477
|
},
|
|
478
|
+
certificate_template: {
|
|
479
|
+
select: { id: true, slug: true },
|
|
480
|
+
},
|
|
430
481
|
_count: { select: { course_enrollment: true } },
|
|
431
482
|
},
|
|
432
483
|
});
|
|
@@ -457,7 +508,14 @@ export class CourseService {
|
|
|
457
508
|
}
|
|
458
509
|
|
|
459
510
|
async update(id: number, dto: UpdateCourseDto) {
|
|
460
|
-
const {
|
|
511
|
+
const {
|
|
512
|
+
categorySlugs,
|
|
513
|
+
logoFileId,
|
|
514
|
+
bannerFileId,
|
|
515
|
+
instructorIds,
|
|
516
|
+
certificateModel,
|
|
517
|
+
...data
|
|
518
|
+
} = dto;
|
|
461
519
|
let existingSlug: string | undefined;
|
|
462
520
|
|
|
463
521
|
if (
|
|
@@ -478,6 +536,8 @@ export class CourseService {
|
|
|
478
536
|
data.title !== undefined
|
|
479
537
|
? this.resolveCourseTitle(data.title, resolvedSlug ?? existingSlug ?? '')
|
|
480
538
|
: undefined;
|
|
539
|
+
const certificateTemplateId =
|
|
540
|
+
await this.resolveCertificateTemplateId(certificateModel);
|
|
481
541
|
|
|
482
542
|
const categories = categorySlugs?.length
|
|
483
543
|
? await this.prisma.category.findMany({
|
|
@@ -535,6 +595,9 @@ export class CourseService {
|
|
|
535
595
|
...(data.certificateWorkload !== undefined && {
|
|
536
596
|
certificate_workload: data.certificateWorkload,
|
|
537
597
|
}),
|
|
598
|
+
...(certificateTemplateId !== undefined && {
|
|
599
|
+
certificate_template_id: certificateTemplateId,
|
|
600
|
+
}),
|
|
538
601
|
...(data.primaryColor !== undefined && {
|
|
539
602
|
primary_color: data.primaryColor,
|
|
540
603
|
}),
|
|
@@ -589,6 +652,9 @@ export class CourseService {
|
|
|
589
652
|
},
|
|
590
653
|
},
|
|
591
654
|
},
|
|
655
|
+
certificate_template: {
|
|
656
|
+
select: { id: true, slug: true },
|
|
657
|
+
},
|
|
592
658
|
_count: { select: { course_enrollment: true } },
|
|
593
659
|
},
|
|
594
660
|
});
|
|
@@ -731,6 +797,11 @@ export class CourseService {
|
|
|
731
797
|
averageCompletion: Math.round(metrics?.averageCompletion ?? 0),
|
|
732
798
|
certificatesIssued:
|
|
733
799
|
metrics?.certificatesIssued ?? c._count?.certificate ?? 0,
|
|
800
|
+
certificateModel:
|
|
801
|
+
c.certificate_template?.slug ??
|
|
802
|
+
(c.certificate_template_id
|
|
803
|
+
? String(c.certificate_template_id)
|
|
804
|
+
: null),
|
|
734
805
|
progressByModule: metrics?.progressByModule ?? [],
|
|
735
806
|
instructors: (c.course_instructor ?? []).map((ci: any) => ({
|
|
736
807
|
id: ci.instructor_id,
|
|
@@ -102,6 +102,7 @@ export class InstructorService {
|
|
|
102
102
|
user_id: true,
|
|
103
103
|
user: {
|
|
104
104
|
select: {
|
|
105
|
+
photo_id: true,
|
|
105
106
|
role_user: {
|
|
106
107
|
where: { role: { slug: 'lms-instructor' } },
|
|
107
108
|
select: { id: true },
|
|
@@ -181,6 +182,7 @@ export class InstructorService {
|
|
|
181
182
|
personId: row.person_id,
|
|
182
183
|
name: row.person?.name?.trim() || `Instructor #${row.id}`,
|
|
183
184
|
avatarId: row.person?.avatar_id ?? null,
|
|
185
|
+
userPhotoId: personUser?.user?.photo_id ?? null,
|
|
184
186
|
email: this.getPrimaryContactValue(
|
|
185
187
|
row.person?.contact ?? [],
|
|
186
188
|
['EMAIL'],
|
|
@@ -12,6 +12,80 @@ export class TrainingService {
|
|
|
12
12
|
private readonly integrationApi: IntegrationDeveloperApiService,
|
|
13
13
|
) {}
|
|
14
14
|
|
|
15
|
+
private normalizeProgressMode(value?: string | null) {
|
|
16
|
+
if (!value) return undefined;
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
|
|
19
|
+
if (normalized === 'free' || normalized === 'livre') return 'free';
|
|
20
|
+
if (normalized === 'sequential' || normalized === 'sequencial') {
|
|
21
|
+
return 'sequential';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private progressModeToPt(value?: string | null) {
|
|
28
|
+
return this.normalizeProgressMode(value) === 'free' ? 'livre' : 'sequencial';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private async getTrainingExtras(ids: number[]) {
|
|
32
|
+
const extrasById = new Map<number, { progress_mode?: 'sequential' | 'free' | null }>();
|
|
33
|
+
|
|
34
|
+
const normalizedIds = ids
|
|
35
|
+
.map((id) => Number(id))
|
|
36
|
+
.filter((id) => Number.isInteger(id) && id > 0);
|
|
37
|
+
|
|
38
|
+
if (normalizedIds.length === 0) {
|
|
39
|
+
return extrasById;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const rows = (await this.prisma.$queryRawUnsafe(
|
|
44
|
+
`
|
|
45
|
+
SELECT id, progress_mode
|
|
46
|
+
FROM learning_path
|
|
47
|
+
WHERE id IN (${normalizedIds.join(',')})
|
|
48
|
+
`,
|
|
49
|
+
)) as Array<{
|
|
50
|
+
id: number;
|
|
51
|
+
progress_mode: 'sequential' | 'free' | null;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
extrasById.set(row.id, {
|
|
56
|
+
progress_mode: row.progress_mode,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return extrasById;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return extrasById;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async persistTrainingProgressMode(
|
|
67
|
+
id: number,
|
|
68
|
+
progressMode?: 'sequential' | 'free',
|
|
69
|
+
) {
|
|
70
|
+
if (progressMode === undefined) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await this.prisma.$executeRawUnsafe(
|
|
76
|
+
`
|
|
77
|
+
UPDATE learning_path
|
|
78
|
+
SET progress_mode = $1
|
|
79
|
+
WHERE id = $2
|
|
80
|
+
`,
|
|
81
|
+
progressMode,
|
|
82
|
+
id,
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
// Some environments may still be behind the current learning_path schema.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
15
89
|
async list(params: {
|
|
16
90
|
page?: number;
|
|
17
91
|
pageSize?: number;
|
|
@@ -110,12 +184,14 @@ export class TrainingService {
|
|
|
110
184
|
this.prisma.learning_path.count({ where }),
|
|
111
185
|
]);
|
|
112
186
|
|
|
187
|
+
const extrasById = await this.getTrainingExtras(paths.map((path) => path.id));
|
|
188
|
+
|
|
113
189
|
return {
|
|
114
190
|
total,
|
|
115
191
|
page,
|
|
116
192
|
pageSize,
|
|
117
193
|
lastPage: Math.max(1, Math.ceil(total / pageSize)),
|
|
118
|
-
data: paths.map((path) => this.mapTraining(path)),
|
|
194
|
+
data: paths.map((path) => this.mapTraining(path, extrasById.get(path.id))),
|
|
119
195
|
};
|
|
120
196
|
}
|
|
121
197
|
|
|
@@ -179,7 +255,9 @@ export class TrainingService {
|
|
|
179
255
|
});
|
|
180
256
|
|
|
181
257
|
if (!path) return null;
|
|
182
|
-
|
|
258
|
+
|
|
259
|
+
const extrasById = await this.getTrainingExtras([id]);
|
|
260
|
+
return this.mapTraining(path, extrasById.get(id));
|
|
183
261
|
}
|
|
184
262
|
|
|
185
263
|
async create(dto: CreateTrainingDto) {
|
|
@@ -187,6 +265,8 @@ export class TrainingService {
|
|
|
187
265
|
const stepItems = this.resolveIncomingSteps(dto);
|
|
188
266
|
const normalizedLevel = this.normalizeLevel(dto.level) ?? 'beginner';
|
|
189
267
|
const normalizedStatus = this.normalizeStatus(dto.status) ?? 'draft';
|
|
268
|
+
const normalizedProgressMode =
|
|
269
|
+
this.normalizeProgressMode(dto.progressMode) ?? 'sequential';
|
|
190
270
|
|
|
191
271
|
const created = await this.prisma.learning_path.create({
|
|
192
272
|
data: {
|
|
@@ -242,7 +322,14 @@ export class TrainingService {
|
|
|
242
322
|
},
|
|
243
323
|
});
|
|
244
324
|
|
|
245
|
-
|
|
325
|
+
await this.persistTrainingProgressMode(created.id, normalizedProgressMode);
|
|
326
|
+
|
|
327
|
+
const extrasById = await this.getTrainingExtras([created.id]);
|
|
328
|
+
|
|
329
|
+
const trainingResult = this.mapTraining(
|
|
330
|
+
created,
|
|
331
|
+
extrasById.get(created.id),
|
|
332
|
+
);
|
|
246
333
|
|
|
247
334
|
await this.integrationApi.publishEvent({
|
|
248
335
|
eventName: 'lms.training.created',
|
|
@@ -262,6 +349,10 @@ export class TrainingService {
|
|
|
262
349
|
dto.level !== undefined ? this.normalizeLevel(dto.level) : undefined;
|
|
263
350
|
const normalizedStatus =
|
|
264
351
|
dto.status !== undefined ? this.normalizeStatus(dto.status) : undefined;
|
|
352
|
+
const normalizedProgressMode =
|
|
353
|
+
dto.progressMode !== undefined
|
|
354
|
+
? this.normalizeProgressMode(dto.progressMode)
|
|
355
|
+
: undefined;
|
|
265
356
|
|
|
266
357
|
if (stepsWereProvided) {
|
|
267
358
|
await this.prisma.learning_path_step.deleteMany({
|
|
@@ -329,7 +420,11 @@ export class TrainingService {
|
|
|
329
420
|
},
|
|
330
421
|
});
|
|
331
422
|
|
|
332
|
-
|
|
423
|
+
await this.persistTrainingProgressMode(id, normalizedProgressMode);
|
|
424
|
+
|
|
425
|
+
const extrasById = await this.getTrainingExtras([id]);
|
|
426
|
+
|
|
427
|
+
const updateTrainingResult = this.mapTraining(updated, extrasById.get(id));
|
|
333
428
|
|
|
334
429
|
await this.integrationApi.publishEvent({
|
|
335
430
|
eventName: 'lms.training.updated',
|
|
@@ -356,7 +451,10 @@ export class TrainingService {
|
|
|
356
451
|
return { success: true };
|
|
357
452
|
}
|
|
358
453
|
|
|
359
|
-
private mapTraining(
|
|
454
|
+
private mapTraining(
|
|
455
|
+
path: any,
|
|
456
|
+
extras?: { progress_mode?: 'sequential' | 'free' | null },
|
|
457
|
+
) {
|
|
360
458
|
const steps = [...(path.learning_path_step ?? [])].sort(
|
|
361
459
|
(a: any, b: any) => (a?.order ?? 0) - (b?.order ?? 0),
|
|
362
460
|
);
|
|
@@ -419,6 +517,7 @@ export class TrainingService {
|
|
|
419
517
|
courseIds,
|
|
420
518
|
examIds,
|
|
421
519
|
items,
|
|
520
|
+
progressionMode: this.progressModeToPt(extras?.progress_mode),
|
|
422
521
|
cargaTotal,
|
|
423
522
|
alunos: path._count?.learning_path_enrollment ?? 0,
|
|
424
523
|
status: this.statusToPt(path.status),
|