@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.
Files changed (53) hide show
  1. package/dist/class-group/class-group.controller.d.ts +11 -11
  2. package/dist/class-group/class-group.service.d.ts +11 -11
  3. package/dist/course/course.controller.d.ts +6 -1
  4. package/dist/course/course.controller.d.ts.map +1 -1
  5. package/dist/course/course.controller.js +19 -2
  6. package/dist/course/course.controller.js.map +1 -1
  7. package/dist/course/course.service.d.ts +6 -0
  8. package/dist/course/course.service.d.ts.map +1 -1
  9. package/dist/course/course.service.js +51 -8
  10. package/dist/course/course.service.js.map +1 -1
  11. package/dist/course/dto/create-course.dto.d.ts +1 -0
  12. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  13. package/dist/course/dto/create-course.dto.js +5 -0
  14. package/dist/course/dto/create-course.dto.js.map +1 -1
  15. package/dist/enterprise/enterprise.controller.d.ts +12 -12
  16. package/dist/enterprise/enterprise.service.d.ts +12 -12
  17. package/dist/enterprise/training/training-admin.controller.d.ts +7 -7
  18. package/dist/enterprise/training/training-admin.service.d.ts +7 -7
  19. package/dist/evaluation/evaluation.controller.d.ts +6 -6
  20. package/dist/evaluation/evaluation.service.d.ts +6 -6
  21. package/dist/instructor/instructor.controller.d.ts +1 -0
  22. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  23. package/dist/instructor/instructor.service.d.ts +2 -0
  24. package/dist/instructor/instructor.service.d.ts.map +1 -1
  25. package/dist/instructor/instructor.service.js +9 -7
  26. package/dist/instructor/instructor.service.js.map +1 -1
  27. package/dist/training/dto/create-training.dto.d.ts +1 -0
  28. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  29. package/dist/training/dto/create-training.dto.js +5 -0
  30. package/dist/training/dto/create-training.dto.js.map +1 -1
  31. package/dist/training/training.controller.d.ts +4 -0
  32. package/dist/training/training.controller.d.ts.map +1 -1
  33. package/dist/training/training.service.d.ts +8 -0
  34. package/dist/training/training.service.d.ts.map +1 -1
  35. package/dist/training/training.service.js +71 -6
  36. package/dist/training/training.service.js.map +1 -1
  37. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +38 -9
  38. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +33 -6
  39. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
  40. package/hedhog/frontend/app/classes/page.tsx.ejs +28 -6
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1 -1
  42. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +25 -18
  43. package/hedhog/frontend/app/paths/page.tsx.ejs +68 -5
  44. package/hedhog/frontend/app/training/page.tsx.ejs +70 -6
  45. package/hedhog/frontend/messages/pt.json +14 -1
  46. package/hedhog/table/learning_path.yaml +4 -0
  47. package/package.json +6 -6
  48. package/src/course/course.controller.ts +18 -0
  49. package/src/course/course.service.ts +73 -2
  50. package/src/course/dto/create-course.dto.ts +4 -0
  51. package/src/instructor/instructor.service.ts +2 -0
  52. package/src/training/dto/create-training.dto.ts +4 -0
  53. 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.338",
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/api-locale": "0.0.14",
17
- "@hed-hog/contact": "0.0.338",
18
- "@hed-hog/category": "0.0.338",
19
- "@hed-hog/core": "0.0.338",
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 { categorySlugs, logoFileId, bannerFileId, instructorIds, ...data } = dto;
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 { categorySlugs, logoFileId, bannerFileId, instructorIds, ...data } = dto;
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,
@@ -99,6 +99,10 @@ export class CreateCourseDto {
99
99
  @IsOptional()
100
100
  categorySlugs?: string[];
101
101
 
102
+ @IsString()
103
+ @IsOptional()
104
+ certificateModel?: string | null;
105
+
102
106
  @IsArray()
103
107
  @IsInt({ each: true })
104
108
  @IsOptional()
@@ -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'],
@@ -58,6 +58,10 @@ export class CreateTrainingDto {
58
58
  @IsOptional()
59
59
  status?: 'draft' | 'active' | 'archived';
60
60
 
61
+ @IsEnum(['sequential', 'free'])
62
+ @IsOptional()
63
+ progressMode?: 'sequential' | 'free';
64
+
61
65
  @IsArray()
62
66
  @IsInt({ each: true })
63
67
  @IsOptional()
@@ -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
- return this.mapTraining(path);
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
- const trainingResult = this.mapTraining(created);
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
- const updateTrainingResult = this.mapTraining(updated);
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(path: any) {
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),