@hed-hog/lms 0.0.325 → 0.0.327

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 (69) hide show
  1. package/dist/course/course.service.d.ts +3 -1
  2. package/dist/course/course.service.d.ts.map +1 -1
  3. package/dist/course/course.service.js +35 -5
  4. package/dist/course/course.service.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +9 -0
  10. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -0
  11. package/dist/instructor/dto/create-instructor-skill.dto.js +48 -0
  12. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -0
  13. package/dist/instructor/dto/create-instructor.dto.d.ts +2 -0
  14. package/dist/instructor/dto/create-instructor.dto.d.ts.map +1 -1
  15. package/dist/instructor/dto/create-instructor.dto.js +12 -0
  16. package/dist/instructor/dto/create-instructor.dto.js.map +1 -1
  17. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +9 -0
  18. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -0
  19. package/dist/instructor/dto/update-instructor-skill.dto.js +50 -0
  20. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -0
  21. package/dist/instructor/dto/update-instructor.dto.d.ts +2 -0
  22. package/dist/instructor/dto/update-instructor.dto.d.ts.map +1 -1
  23. package/dist/instructor/dto/update-instructor.dto.js +12 -0
  24. package/dist/instructor/dto/update-instructor.dto.js.map +1 -1
  25. package/dist/instructor/instructor-skill.controller.d.ts +38 -0
  26. package/dist/instructor/instructor-skill.controller.d.ts.map +1 -0
  27. package/dist/instructor/instructor-skill.controller.js +89 -0
  28. package/dist/instructor/instructor-skill.controller.js.map +1 -0
  29. package/dist/instructor/instructor-skill.service.d.ts +48 -0
  30. package/dist/instructor/instructor-skill.service.d.ts.map +1 -0
  31. package/dist/instructor/instructor-skill.service.js +203 -0
  32. package/dist/instructor/instructor-skill.service.js.map +1 -0
  33. package/dist/instructor/instructor.controller.d.ts +26 -0
  34. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  35. package/dist/instructor/instructor.module.d.ts.map +1 -1
  36. package/dist/instructor/instructor.module.js +4 -2
  37. package/dist/instructor/instructor.module.js.map +1 -1
  38. package/dist/instructor/instructor.service.d.ts +35 -0
  39. package/dist/instructor/instructor.service.d.ts.map +1 -1
  40. package/dist/instructor/instructor.service.js +132 -11
  41. package/dist/instructor/instructor.service.js.map +1 -1
  42. package/dist/training/training.service.d.ts +3 -1
  43. package/dist/training/training.service.d.ts.map +1 -1
  44. package/dist/training/training.service.js +34 -4
  45. package/dist/training/training.service.js.map +1 -1
  46. package/hedhog/data/integration_event_catalog.yaml +219 -0
  47. package/hedhog/data/menu.yaml +23 -6
  48. package/hedhog/data/route.yaml +45 -0
  49. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +1 -1
  50. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
  51. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +547 -0
  52. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +845 -239
  53. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +9 -0
  54. package/hedhog/frontend/app/instructors/page.tsx.ejs +69 -20
  55. package/hedhog/table/instructor.yaml +5 -0
  56. package/hedhog/table/instructor_skill.yaml +26 -0
  57. package/hedhog/table/instructor_skill_assignment.yaml +22 -0
  58. package/package.json +6 -6
  59. package/src/course/course.service.ts +38 -4
  60. package/src/index.ts +2 -0
  61. package/src/instructor/dto/create-instructor-skill.dto.ts +28 -0
  62. package/src/instructor/dto/create-instructor.dto.ts +20 -8
  63. package/src/instructor/dto/update-instructor-skill.dto.ts +30 -0
  64. package/src/instructor/dto/update-instructor.dto.ts +18 -6
  65. package/src/instructor/instructor-skill.controller.ts +60 -0
  66. package/src/instructor/instructor-skill.service.ts +214 -0
  67. package/src/instructor/instructor.module.ts +4 -2
  68. package/src/instructor/instructor.service.ts +148 -0
  69. package/src/training/training.service.ts +38 -4
@@ -0,0 +1,214 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable, NotFoundException } from '@nestjs/common';
3
+ import { CreateInstructorSkillDto } from './dto/create-instructor-skill.dto';
4
+ import { UpdateInstructorSkillDto } from './dto/update-instructor-skill.dto';
5
+
6
+ @Injectable()
7
+ export class InstructorSkillService {
8
+ constructor(private readonly prisma: PrismaService) {}
9
+
10
+ private get skillClient() {
11
+ return (this.prisma as any).instructor_skill;
12
+ }
13
+
14
+ private get localeClient() {
15
+ return (this.prisma as any).instructor_skill_locale;
16
+ }
17
+
18
+ async list(params: { page?: number; pageSize?: number; search?: string }) {
19
+ const page = Math.max(Number(params.page) || 1, 1);
20
+ const pageSize = Math.max(Number(params.pageSize) || 20, 1);
21
+ const skip = (page - 1) * pageSize;
22
+ const search = params.search?.trim();
23
+
24
+ const where: any = {};
25
+
26
+ if (search) {
27
+ where.OR = [
28
+ { slug: { contains: search, mode: 'insensitive' } },
29
+ {
30
+ instructor_skill_locale: {
31
+ some: { name: { contains: search, mode: 'insensitive' } },
32
+ },
33
+ },
34
+ ];
35
+ }
36
+
37
+ const [rows, total] = await Promise.all([
38
+ this.skillClient.findMany({
39
+ where,
40
+ skip,
41
+ take: pageSize,
42
+ include: {
43
+ instructor_skill_locale: {
44
+ select: { locale_id: true, name: true, description: true },
45
+ },
46
+ },
47
+ orderBy: { slug: 'asc' },
48
+ }),
49
+ this.skillClient.count({ where }),
50
+ ]);
51
+
52
+ return {
53
+ data: (rows as any[]).map((row) => this.mapRow(row)),
54
+ total,
55
+ page,
56
+ pageSize,
57
+ };
58
+ }
59
+
60
+ async getAll() {
61
+ const rows = await this.skillClient.findMany({
62
+ where: { status: 'active' },
63
+ include: {
64
+ instructor_skill_locale: {
65
+ select: { locale_id: true, name: true },
66
+ },
67
+ },
68
+ orderBy: { slug: 'asc' },
69
+ });
70
+
71
+ return (rows as any[]).map((row) => this.mapRow(row));
72
+ }
73
+
74
+ async create(dto: CreateInstructorSkillDto) {
75
+ const skill = await this.skillClient.create({
76
+ data: {
77
+ slug: dto.slug,
78
+ status: dto.status ?? 'active',
79
+ },
80
+ });
81
+
82
+ await this.syncLocales(skill.id, dto);
83
+
84
+ return this.getSkillById(skill.id);
85
+ }
86
+
87
+ async update(id: number, dto: UpdateInstructorSkillDto) {
88
+ const skill = await this.skillClient.findUnique({
89
+ where: { id },
90
+ select: { id: true },
91
+ });
92
+
93
+ if (!skill) {
94
+ throw new NotFoundException('Instructor skill not found');
95
+ }
96
+
97
+ const data: any = {};
98
+
99
+ if (dto.slug !== undefined) data.slug = dto.slug;
100
+ if (dto.status !== undefined) data.status = dto.status;
101
+
102
+ if (Object.keys(data).length > 0) {
103
+ await this.skillClient.update({ where: { id }, data });
104
+ }
105
+
106
+ await this.syncLocales(id, dto);
107
+
108
+ return this.getSkillById(id);
109
+ }
110
+
111
+ async delete(id: number) {
112
+ const skill = await this.skillClient.findUnique({
113
+ where: { id },
114
+ select: { id: true },
115
+ });
116
+
117
+ if (!skill) {
118
+ throw new NotFoundException('Instructor skill not found');
119
+ }
120
+
121
+ await this.skillClient.delete({ where: { id } });
122
+ }
123
+
124
+ private async getSkillById(id: number) {
125
+ const row = await this.skillClient.findUnique({
126
+ where: { id },
127
+ include: {
128
+ instructor_skill_locale: {
129
+ select: { locale_id: true, name: true, description: true },
130
+ },
131
+ },
132
+ });
133
+
134
+ if (!row) {
135
+ throw new NotFoundException('Instructor skill not found');
136
+ }
137
+
138
+ return this.mapRow(row);
139
+ }
140
+
141
+ private mapRow(row: any) {
142
+ return {
143
+ id: row.id,
144
+ slug: row.slug,
145
+ status: row.status,
146
+ locales: row.instructor_skill_locale ?? [],
147
+ };
148
+ }
149
+
150
+ private async syncLocales(
151
+ skillId: number,
152
+ dto: CreateInstructorSkillDto | UpdateInstructorSkillDto,
153
+ ) {
154
+ const localeIds = await this.resolveLocaleIds();
155
+
156
+ if (dto.namePt !== undefined && localeIds.pt !== null) {
157
+ const existing = await this.localeClient.findFirst({
158
+ where: { instructor_skill_id: skillId, locale_id: localeIds.pt },
159
+ select: { id: true },
160
+ });
161
+
162
+ if (existing) {
163
+ await this.localeClient.update({
164
+ where: { id: existing.id },
165
+ data: { name: dto.namePt, description: dto.descriptionPt ?? null },
166
+ });
167
+ } else {
168
+ await this.localeClient.create({
169
+ data: {
170
+ instructor_skill_id: skillId,
171
+ locale_id: localeIds.pt,
172
+ name: dto.namePt,
173
+ description: dto.descriptionPt ?? null,
174
+ },
175
+ });
176
+ }
177
+ }
178
+
179
+ if (dto.nameEn !== undefined && localeIds.en !== null) {
180
+ const existing = await this.localeClient.findFirst({
181
+ where: { instructor_skill_id: skillId, locale_id: localeIds.en },
182
+ select: { id: true },
183
+ });
184
+
185
+ if (existing) {
186
+ await this.localeClient.update({
187
+ where: { id: existing.id },
188
+ data: { name: dto.nameEn, description: dto.descriptionEn ?? null },
189
+ });
190
+ } else {
191
+ await this.localeClient.create({
192
+ data: {
193
+ instructor_skill_id: skillId,
194
+ locale_id: localeIds.en,
195
+ name: dto.nameEn,
196
+ description: dto.descriptionEn ?? null,
197
+ },
198
+ });
199
+ }
200
+ }
201
+ }
202
+
203
+ private async resolveLocaleIds() {
204
+ const locales = await this.prisma.locale.findMany({
205
+ where: { code: { in: ['pt', 'en'] } },
206
+ select: { id: true, code: true },
207
+ });
208
+
209
+ return {
210
+ pt: locales.find((l) => l.code === 'pt')?.id ?? null,
211
+ en: locales.find((l) => l.code === 'en')?.id ?? null,
212
+ };
213
+ }
214
+ }
@@ -1,12 +1,14 @@
1
1
  import { PrismaModule } from '@hed-hog/api-prisma';
2
2
  import { forwardRef, Module } from '@nestjs/common';
3
+ import { InstructorSkillController } from './instructor-skill.controller';
4
+ import { InstructorSkillService } from './instructor-skill.service';
3
5
  import { InstructorController } from './instructor.controller';
4
6
  import { InstructorService } from './instructor.service';
5
7
 
6
8
  @Module({
7
9
  imports: [forwardRef(() => PrismaModule)],
8
- controllers: [InstructorController],
9
- providers: [InstructorService],
10
+ controllers: [InstructorController, InstructorSkillController],
11
+ providers: [InstructorService, InstructorSkillService],
10
12
  exports: [forwardRef(() => InstructorService)],
11
13
  })
12
14
  export class InstructorModule {}
@@ -85,6 +85,17 @@ export class InstructorService {
85
85
  id: true,
86
86
  name: true,
87
87
  avatar_id: true,
88
+ contact: {
89
+ where: {
90
+ contact_type: {
91
+ code: { in: ['EMAIL', 'PHONE', 'MOBILE', 'WHATSAPP'] },
92
+ },
93
+ },
94
+ include: {
95
+ contact_type: { select: { code: true } },
96
+ },
97
+ orderBy: [{ is_primary: 'desc' as const }, { id: 'asc' as const }],
98
+ },
88
99
  person_user: {
89
100
  take: 1,
90
101
  select: {
@@ -118,6 +129,48 @@ export class InstructorService {
118
129
  this.prisma.instructor.count({ where }),
119
130
  ]);
120
131
 
132
+ const instructorIds = rows.map((r) => r.id);
133
+ let hourlyRateMap = new Map<number, number | null>();
134
+ let skillsMap = new Map<number, Array<{ id: number; slug: string; name: string }>>();
135
+
136
+ if (instructorIds.length > 0) {
137
+ const [rateRows, assignmentRows] = (await Promise.all([
138
+ this.prisma.$queryRaw(
139
+ Prisma.sql`SELECT id, hourly_rate FROM instructor WHERE id IN (${Prisma.join(instructorIds)})`,
140
+ ),
141
+ (this.prisma as any).instructor_skill_assignment.findMany({
142
+ where: { instructor_id: { in: instructorIds } },
143
+ select: {
144
+ instructor_id: true,
145
+ instructor_skill: {
146
+ select: {
147
+ id: true,
148
+ slug: true,
149
+ instructor_skill_locale: {
150
+ take: 1,
151
+ select: { name: true },
152
+ },
153
+ },
154
+ },
155
+ },
156
+ }),
157
+ ])) as [Array<{ id: number; hourly_rate: number | null }>, any[]];
158
+
159
+ for (const r of rateRows) {
160
+ hourlyRateMap.set(Number(r.id), r.hourly_rate !== null ? Number(r.hourly_rate) : null);
161
+ }
162
+
163
+ for (const a of assignmentRows) {
164
+ const existing = skillsMap.get(a.instructor_id) ?? [];
165
+ existing.push({
166
+ id: a.instructor_skill.id,
167
+ slug: a.instructor_skill.slug,
168
+ name: a.instructor_skill.instructor_skill_locale?.[0]?.name ?? a.instructor_skill.slug,
169
+ });
170
+ skillsMap.set(a.instructor_id, existing);
171
+ }
172
+ }
173
+
121
174
  return {
122
175
  data: rows
123
176
  .map((row) => {
@@ -127,12 +180,22 @@ export class InstructorService {
127
180
  personId: row.person_id,
128
181
  name: row.person?.name?.trim() || `Instructor #${row.id}`,
129
182
  avatarId: row.person?.avatar_id ?? null,
183
+ email: this.getPrimaryContactValue(
184
+ row.person?.contact ?? [],
185
+ ['EMAIL'],
186
+ ),
187
+ phone: this.getPrimaryContactValue(
188
+ row.person?.contact ?? [],
189
+ ['PHONE', 'MOBILE', 'WHATSAPP'],
190
+ ),
130
191
  userId: personUser?.user_id ?? null,
131
192
  hasTrainingAccess: (personUser?.user?.role_user?.length ?? 0) > 0,
132
193
  status: row.status,
194
+ hourlyRate: hourlyRateMap.get(row.id) ?? null,
133
195
  qualificationSlugs: [...new Set(row.instructor_qualification_assignment
134
196
  .map((assignment) => assignment.instructor_qualification.slug)
135
197
  .sort())],
198
+ skills: skillsMap.get(row.id) ?? [],
136
199
  };
137
200
  })
138
201
  .sort((left, right) => left.name.localeCompare(right.name)),
@@ -217,6 +280,12 @@ export class InstructorService {
217
280
  dto.qualificationSlugs,
218
281
  );
219
282
 
283
+ if (dto.hourlyRate !== undefined) {
284
+ await tx.$executeRaw`UPDATE instructor SET hourly_rate = ${dto.hourlyRate ?? null} WHERE id = ${instructor.id}`;
285
+ }
286
+
287
+ await this.syncInstructorSkills(tx, instructor.id, dto.skillSlugs ?? []);
288
+
220
289
  return this.getById(instructor.id, tx);
221
290
  });
222
291
  }
@@ -284,6 +353,14 @@ export class InstructorService {
284
353
  );
285
354
  }
286
355
 
356
+ if (dto.hourlyRate !== undefined) {
357
+ await tx.$executeRaw`UPDATE instructor SET hourly_rate = ${dto.hourlyRate ?? null} WHERE id = ${id}`;
358
+ }
359
+
360
+ if (dto.skillSlugs !== undefined) {
361
+ await this.syncInstructorSkills(tx, id, dto.skillSlugs);
362
+ }
363
+
287
364
  return this.getById(id, tx);
288
365
  });
289
366
  }
@@ -379,6 +456,36 @@ export class InstructorService {
379
456
 
380
457
  const personUser = instructor.person.person_user?.[0] ?? null;
381
458
 
459
+ const [rateRows, skillAssignments] = (await Promise.all([
460
+ (db as any).$queryRaw`SELECT id, hourly_rate FROM instructor WHERE id = ${id}`,
461
+ (db as any).instructor_skill_assignment.findMany({
462
+ where: { instructor_id: id },
463
+ select: {
464
+ instructor_skill: {
465
+ select: {
466
+ id: true,
467
+ slug: true,
468
+ instructor_skill_locale: {
469
+ take: 1,
470
+ select: { name: true },
471
+ },
472
+ },
473
+ },
474
+ },
475
+ }),
476
+ ])) as [Array<{ id: number; hourly_rate: number | null }>, any[]];
477
+
478
+ const hourlyRate =
479
+ rateRows[0]?.hourly_rate !== null && rateRows[0]?.hourly_rate !== undefined
480
+ ? Number(rateRows[0].hourly_rate)
481
+ : null;
482
+
483
+ const skills = skillAssignments.map((a) => ({
484
+ id: a.instructor_skill.id,
485
+ slug: a.instructor_skill.slug,
486
+ name: a.instructor_skill.instructor_skill_locale?.[0]?.name ?? a.instructor_skill.slug,
487
+ }));
488
+
382
489
  return {
383
490
  id: instructor.id,
384
491
  personId: instructor.person.id,
@@ -393,9 +500,11 @@ export class InstructorService {
393
500
  'WHATSAPP',
394
501
  ]),
395
502
  status: instructor.status,
503
+ hourlyRate,
396
504
  qualificationSlugs: [...new Set(instructor.instructor_qualification_assignment
397
505
  .map((assignment) => assignment.instructor_qualification.slug)
398
506
  .sort())],
507
+ skills,
399
508
  };
400
509
  }
401
510
 
@@ -773,4 +882,43 @@ export class InstructorService {
773
882
  },
774
883
  });
775
884
  }
885
+
886
+ private async syncInstructorSkills(
887
+ db: DbClient,
888
+ instructorId: number,
889
+ slugs: string[],
890
+ ) {
891
+ const normalizedSlugs = [...new Set((slugs ?? []).map((s) => s?.trim()).filter(Boolean))];
892
+
893
+ if (normalizedSlugs.length === 0) {
894
+ await (db as any).instructor_skill_assignment.deleteMany({
895
+ where: { instructor_id: instructorId },
896
+ });
897
+ return;
898
+ }
899
+
900
+ const skillRows = await (db as any).instructor_skill.findMany({
901
+ where: { slug: { in: normalizedSlugs } },
902
+ select: { id: true },
903
+ });
904
+
905
+ const skillIds = (skillRows as any[]).map((r) => r.id);
906
+
907
+ await (db as any).instructor_skill_assignment.deleteMany({
908
+ where: {
909
+ instructor_id: instructorId,
910
+ skill_id: { notIn: skillIds },
911
+ },
912
+ });
913
+
914
+ if (skillIds.length > 0) {
915
+ await (db as any).instructor_skill_assignment.createMany({
916
+ data: skillIds.map((skillId: number) => ({
917
+ instructor_id: instructorId,
918
+ skill_id: skillId,
919
+ })),
920
+ skipDuplicates: true,
921
+ });
922
+ }
923
+ }
776
924
  }
@@ -1,11 +1,16 @@
1
1
  import { PrismaService } from '@hed-hog/api-prisma';
2
- import { Injectable } from '@nestjs/common';
2
+ import { IntegrationDeveloperApiService } from '@hed-hog/core';
3
+ import { Inject, Injectable, forwardRef } from '@nestjs/common';
3
4
  import { CreateTrainingDto, LearningPathItemDto } from './dto/create-training.dto';
4
5
  import { UpdateTrainingDto } from './dto/update-training.dto';
5
6
 
6
7
  @Injectable()
7
8
  export class TrainingService {
8
- constructor(private readonly prisma: PrismaService) {}
9
+ constructor(
10
+ private readonly prisma: PrismaService,
11
+ @Inject(forwardRef(() => IntegrationDeveloperApiService))
12
+ private readonly integrationApi: IntegrationDeveloperApiService,
13
+ ) {}
9
14
 
10
15
  async list(params: {
11
16
  page?: number;
@@ -237,7 +242,17 @@ export class TrainingService {
237
242
  },
238
243
  });
239
244
 
240
- return this.mapTraining(created);
245
+ const trainingResult = this.mapTraining(created);
246
+
247
+ await this.integrationApi.publishEvent({
248
+ eventName: 'lms.training.created',
249
+ sourceModule: 'lms',
250
+ aggregateType: 'training',
251
+ aggregateId: String(created.id),
252
+ payload: { id: created.id, title: dto.title, slug, status: normalizedStatus },
253
+ }).catch(() => null);
254
+
255
+ return trainingResult;
241
256
  }
242
257
 
243
258
  async update(id: number, dto: UpdateTrainingDto) {
@@ -314,11 +329,30 @@ export class TrainingService {
314
329
  },
315
330
  });
316
331
 
317
- return this.mapTraining(updated);
332
+ const updateTrainingResult = this.mapTraining(updated);
333
+
334
+ await this.integrationApi.publishEvent({
335
+ eventName: 'lms.training.updated',
336
+ sourceModule: 'lms',
337
+ aggregateType: 'training',
338
+ aggregateId: String(id),
339
+ payload: { id, title: dto.title, status: dto.status },
340
+ }).catch(() => null);
341
+
342
+ return updateTrainingResult;
318
343
  }
319
344
 
320
345
  async remove(id: number) {
321
346
  await this.prisma.learning_path.delete({ where: { id } });
347
+
348
+ await this.integrationApi.publishEvent({
349
+ eventName: 'lms.training.deleted',
350
+ sourceModule: 'lms',
351
+ aggregateType: 'training',
352
+ aggregateId: String(id),
353
+ payload: { id },
354
+ }).catch(() => null);
355
+
322
356
  return { success: true };
323
357
  }
324
358