@hed-hog/lms 0.0.312 → 0.0.315

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 (68) hide show
  1. package/dist/class-group/class-group.controller.d.ts +2 -2
  2. package/dist/class-group/class-group.service.d.ts +2 -2
  3. package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
  4. package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
  5. package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
  6. package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
  7. package/dist/enterprise/enterprise.controller.d.ts +3 -0
  8. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  9. package/dist/enterprise/enterprise.controller.js +14 -0
  10. package/dist/enterprise/enterprise.controller.js.map +1 -1
  11. package/dist/enterprise/enterprise.service.d.ts +3 -0
  12. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  13. package/dist/enterprise/enterprise.service.js +128 -1
  14. package/dist/enterprise/enterprise.service.js.map +1 -1
  15. package/dist/instructor/instructor.controller.d.ts +23 -0
  16. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  17. package/dist/instructor/instructor.controller.js +41 -0
  18. package/dist/instructor/instructor.controller.js.map +1 -1
  19. package/dist/instructor/instructor.service.d.ts +25 -0
  20. package/dist/instructor/instructor.service.d.ts.map +1 -1
  21. package/dist/instructor/instructor.service.js +126 -8
  22. package/dist/instructor/instructor.service.js.map +1 -1
  23. package/hedhog/data/menu.yaml +23 -7
  24. package/hedhog/data/role.yaml +9 -1
  25. package/hedhog/data/route.yaml +54 -0
  26. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
  27. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
  28. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
  29. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
  30. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
  31. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
  32. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
  33. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
  34. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
  35. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
  36. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
  37. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
  42. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
  43. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
  44. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
  45. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
  51. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
  52. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
  54. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
  55. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
  56. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
  57. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
  58. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
  59. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
  60. package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
  61. package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
  62. package/hedhog/table/enterprise_user.yaml +1 -1
  63. package/package.json +8 -8
  64. package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
  65. package/src/enterprise/enterprise.controller.ts +9 -1
  66. package/src/enterprise/enterprise.service.ts +147 -4
  67. package/src/instructor/instructor.controller.ts +36 -9
  68. package/src/instructor/instructor.service.ts +140 -10
@@ -21,7 +21,7 @@ columns:
21
21
  onDelete: SET NULL
22
22
  - name: role
23
23
  type: enum
24
- values: [hr_manager, enterprise_admin, viewer]
24
+ values: [hr_manager, enterprise_admin, viewer, instructor]
25
25
  default: viewer
26
26
  - name: status
27
27
  type: enum
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/lms",
3
- "version": "0.0.312",
3
+ "version": "0.0.315",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,15 +9,15 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
- "@hed-hog/api-prisma": "0.0.6",
13
- "@hed-hog/api-pagination": "0.0.7",
14
- "@hed-hog/contact": "0.0.312",
15
- "@hed-hog/core": "0.0.312",
12
+ "@hed-hog/api": "0.0.6",
13
+ "@hed-hog/contact": "0.0.315",
16
14
  "@hed-hog/api-locale": "0.0.14",
15
+ "@hed-hog/api-pagination": "0.0.7",
17
16
  "@hed-hog/api-types": "0.0.1",
18
- "@hed-hog/finance": "0.0.312",
19
- "@hed-hog/api": "0.0.6",
20
- "@hed-hog/category": "0.0.312"
17
+ "@hed-hog/api-prisma": "0.0.6",
18
+ "@hed-hog/finance": "0.0.315",
19
+ "@hed-hog/category": "0.0.315",
20
+ "@hed-hog/core": "0.0.315"
21
21
  },
22
22
  "exports": {
23
23
  ".": {
@@ -0,0 +1,17 @@
1
+ export type EnterpriseAccessSlug =
2
+ | 'lms-enterprise-admin'
3
+ | 'lms-enterprise-viewer'
4
+ | 'lms-instructor'
5
+ | 'lms-student';
6
+
7
+ export interface EnterpriseProfile {
8
+ enterpriseId: number;
9
+ enterpriseName: string;
10
+ /** Raw value from enterprise_user.role or 'student' for enterprise_student entries */
11
+ role: string;
12
+ /** Normalized access slug used by the training app to determine the home dashboard */
13
+ accessSlug: EnterpriseAccessSlug;
14
+ status: string;
15
+ /** file.id of the enterprise's CRM person avatar, null when not set */
16
+ enterpriseLogoId: number | null;
17
+ }
@@ -1,4 +1,4 @@
1
- import { Role } from '@hed-hog/api';
1
+ import { Public, Role, UserOptional } from '@hed-hog/api';
2
2
  import {
3
3
  Body,
4
4
  Controller,
@@ -57,6 +57,14 @@ export class EnterpriseController {
57
57
  return this.enterpriseService.getCrmOptions();
58
58
  }
59
59
 
60
+ @Public()
61
+ @Get('auth/profiles')
62
+ async getAuthProfiles(@UserOptional() user: any) {
63
+ if (!user?.id) return { profiles: [] };
64
+ const profiles = await this.enterpriseService.getProfilesForUser(user.id);
65
+ return { profiles };
66
+ }
67
+
60
68
  @Get(':id')
61
69
  getById(@Param('id', ParseIntPipe) id: number) {
62
70
  return this.enterpriseService.getById(id);
@@ -1,14 +1,15 @@
1
1
  import { PrismaService } from '@hed-hog/api-prisma';
2
2
  import {
3
- ConflictException,
4
- Injectable,
5
- NotFoundException,
3
+ ConflictException,
4
+ Injectable,
5
+ NotFoundException,
6
6
  } from '@nestjs/common';
7
7
  import { AddEnterpriseClassGroupDto } from './dto/add-enterprise-class-group.dto';
8
8
  import { AddEnterpriseCourseDto } from './dto/add-enterprise-course.dto';
9
9
  import { AddEnterpriseStudentDto } from './dto/add-enterprise-student.dto';
10
10
  import { AddEnterpriseUserDto } from './dto/add-enterprise-user.dto';
11
11
  import { CreateEnterpriseDto } from './dto/create-enterprise.dto';
12
+ import { EnterpriseProfile } from './dto/enterprise-profile.dto';
12
13
  import { UpdateEnterpriseStudentDto } from './dto/update-enterprise-student.dto';
13
14
  import { UpdateEnterpriseUserDto } from './dto/update-enterprise-user.dto';
14
15
  import { UpdateEnterpriseDto } from './dto/update-enterprise.dto';
@@ -192,6 +193,18 @@ export class EnterpriseService {
192
193
  }
193
194
  }
194
195
 
196
+ private async removeRoleUser(userId: number, roleSlug: string) {
197
+ const roleId = await this.resolveRoleId(roleSlug);
198
+ if (!roleId) return;
199
+ const existing = await this.prisma.role_user.findFirst({
200
+ where: { user_id: userId, role_id: roleId },
201
+ select: { id: true },
202
+ });
203
+ if (existing) {
204
+ await this.prisma.role_user.delete({ where: { id: existing.id } });
205
+ }
206
+ }
207
+
195
208
  // ─── Users ───────────────────────────────────────────────────────────────────
196
209
 
197
210
  async listUsers(enterpriseId: number, params: PaginationParams) {
@@ -282,6 +295,7 @@ export class EnterpriseService {
282
295
  // Apply the corresponding global LMS role
283
296
  const globalSlug = ENTERPRISE_ROLE_TO_GLOBAL_SLUG[dto.role];
284
297
  if (globalSlug) await this.upsertRoleUser(dto.user_id, globalSlug);
298
+ await this.upsertRoleUser(dto.user_id, 'lms-training-access');
285
299
 
286
300
  return record;
287
301
  }
@@ -325,9 +339,16 @@ export class EnterpriseService {
325
339
  throw new NotFoundException(
326
340
  `User #${userId} is not linked to enterprise #${enterpriseId}`,
327
341
  );
328
- return this.prisma.enterprise_user.delete({
342
+ const deleted = await this.prisma.enterprise_user.delete({
329
343
  where: { id: existing.id },
330
344
  });
345
+ const remainingLinks = await this.prisma.enterprise_user.count({
346
+ where: { user_id: userId },
347
+ });
348
+ if (remainingLinks === 0) {
349
+ await this.removeRoleUser(userId, 'lms-training-access');
350
+ }
351
+ return deleted;
331
352
  }
332
353
 
333
354
  // ─── Courses ─────────────────────────────────────────────────────────────────
@@ -668,6 +689,128 @@ export class EnterpriseService {
668
689
 
669
690
  // ─── Mapper ──────────────────────────────────────────────────────────────────
670
691
 
692
+ // ─── Training Auth Profiles ──────────────────────────────────────────────────
693
+
694
+ async getProfilesForUser(userId: number): Promise<EnterpriseProfile[]> {
695
+ const ROLE_TO_SLUG: Record<string, string> = {
696
+ hr_manager: 'lms-enterprise-admin',
697
+ enterprise_admin: 'lms-enterprise-admin',
698
+ viewer: 'lms-enterprise-viewer',
699
+ instructor: 'lms-instructor',
700
+ };
701
+
702
+ const [enterpriseUsers, personLinks] = await Promise.all([
703
+ this.prisma.enterprise_user.findMany({
704
+ where: {
705
+ user_id: userId,
706
+ status: { in: ['active', 'pending'] as any[] },
707
+ },
708
+ select: {
709
+ enterprise_id: true,
710
+ role: true,
711
+ status: true,
712
+ enterprise: { select: { name: true, person: { select: { avatar_id: true } } } },
713
+ },
714
+ }),
715
+ this.prisma.person_user.findMany({
716
+ where: { user_id: userId },
717
+ select: { person_id: true },
718
+ }),
719
+ ]);
720
+
721
+ const personIds = personLinks.map((p) => p.person_id);
722
+ const studentEntries =
723
+ personIds.length > 0
724
+ ? await this.prisma.enterprise_student.findMany({
725
+ where: {
726
+ person_id: { in: personIds },
727
+ status: { in: ['active', 'pending'] as any[] },
728
+ },
729
+ select: {
730
+ enterprise_id: true,
731
+ status: true,
732
+ enterprise: { select: { name: true, person: { select: { avatar_id: true } } } },
733
+ },
734
+ })
735
+ : [];
736
+
737
+ // Activate pending records on first access
738
+ const pendingEnterpriseIds = enterpriseUsers
739
+ .filter((eu) => (eu.status as string) === 'pending')
740
+ .map((eu) => eu.enterprise_id);
741
+
742
+ const pendingStudentEnterpriseIds = studentEntries
743
+ .filter((es) => (es.status as string) === 'pending')
744
+ .map((es) => es.enterprise_id);
745
+
746
+ const activations: Promise<any>[] = [];
747
+
748
+ if (pendingEnterpriseIds.length > 0) {
749
+ activations.push(
750
+ this.prisma.enterprise_user.updateMany({
751
+ where: {
752
+ user_id: userId,
753
+ enterprise_id: { in: pendingEnterpriseIds },
754
+ status: 'pending' as any,
755
+ },
756
+ data: { status: 'active' as any },
757
+ }),
758
+ );
759
+ }
760
+
761
+ if (pendingStudentEnterpriseIds.length > 0) {
762
+ activations.push(
763
+ this.prisma.enterprise_student.updateMany({
764
+ where: {
765
+ person_id: { in: personIds },
766
+ enterprise_id: { in: pendingStudentEnterpriseIds },
767
+ status: 'pending' as any,
768
+ },
769
+ data: { status: 'active' as any },
770
+ }),
771
+ );
772
+ }
773
+
774
+ if (activations.length > 0) {
775
+ await Promise.all(activations);
776
+ }
777
+
778
+ const profiles: EnterpriseProfile[] = [];
779
+ const seen = new Set<string>();
780
+
781
+ for (const eu of enterpriseUsers) {
782
+ const accessSlug = ROLE_TO_SLUG[eu.role as string];
783
+ if (!accessSlug) continue;
784
+ const key = `${eu.enterprise_id}:${accessSlug}`;
785
+ if (seen.has(key)) continue;
786
+ seen.add(key);
787
+ profiles.push({
788
+ enterpriseId: eu.enterprise_id,
789
+ enterpriseName: eu.enterprise.name,
790
+ role: eu.role as string,
791
+ accessSlug: accessSlug as EnterpriseProfile['accessSlug'],
792
+ status: 'active',
793
+ enterpriseLogoId: (eu.enterprise as any).person?.avatar_id ?? null,
794
+ });
795
+ }
796
+
797
+ for (const es of studentEntries) {
798
+ const key = `${es.enterprise_id}:lms-student`;
799
+ if (seen.has(key)) continue;
800
+ seen.add(key);
801
+ profiles.push({
802
+ enterpriseId: es.enterprise_id,
803
+ enterpriseName: es.enterprise.name,
804
+ role: 'student',
805
+ accessSlug: 'lms-student',
806
+ status: 'active',
807
+ enterpriseLogoId: (es.enterprise as any).person?.avatar_id ?? null,
808
+ });
809
+ }
810
+
811
+ return profiles;
812
+ }
813
+
671
814
  private mapEnterprise(
672
815
  e: any,
673
816
  extra?: {
@@ -1,13 +1,16 @@
1
- import { Role } from '@hed-hog/api';
1
+ import { Role, User } from '@hed-hog/api';
2
2
  import {
3
- Body,
4
- Controller,
5
- Get,
6
- Param,
7
- ParseIntPipe,
8
- Patch,
9
- Post,
10
- Query,
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ Get,
7
+ HttpCode,
8
+ HttpStatus,
9
+ Param,
10
+ ParseIntPipe,
11
+ Patch,
12
+ Post,
13
+ Query,
11
14
  } from '@nestjs/common';
12
15
  import { CreateInstructorDto } from './dto/create-instructor.dto';
13
16
  import { UpdateInstructorDto } from './dto/update-instructor.dto';
@@ -35,6 +38,16 @@ export class InstructorController {
35
38
  });
36
39
  }
37
40
 
41
+ @Get('stats')
42
+ getStats() {
43
+ return this.instructorService.getStats();
44
+ }
45
+
46
+ @Get('me')
47
+ getMe(@User('id') userId: number) {
48
+ return this.instructorService.getMe(userId);
49
+ }
50
+
38
51
  @Post()
39
52
  create(@Body() dto: CreateInstructorDto) {
40
53
  return this.instructorService.create(dto);
@@ -45,6 +58,14 @@ export class InstructorController {
45
58
  return this.instructorService.getById(id);
46
59
  }
47
60
 
61
+ @Patch(':id/training-access')
62
+ setTrainingAccess(
63
+ @Param('id', ParseIntPipe) id: number,
64
+ @Body('enabled') enabled: boolean,
65
+ ) {
66
+ return this.instructorService.setTrainingAccess(id, enabled);
67
+ }
68
+
48
69
  @Patch(':id')
49
70
  update(
50
71
  @Param('id', ParseIntPipe) id: number,
@@ -53,6 +74,12 @@ export class InstructorController {
53
74
  return this.instructorService.update(id, dto);
54
75
  }
55
76
 
77
+ @Delete(':id')
78
+ @HttpCode(HttpStatus.NO_CONTENT)
79
+ remove(@Param('id', ParseIntPipe) id: number) {
80
+ return this.instructorService.delete(id);
81
+ }
82
+
56
83
  private toArray(value?: string | string[]) {
57
84
  if (Array.isArray(value)) {
58
85
  return value
@@ -85,6 +85,20 @@ export class InstructorService {
85
85
  id: true,
86
86
  name: true,
87
87
  avatar_id: true,
88
+ person_user: {
89
+ take: 1,
90
+ select: {
91
+ user_id: true,
92
+ user: {
93
+ select: {
94
+ role_user: {
95
+ where: { role: { slug: 'lms-instructor' } },
96
+ select: { id: true },
97
+ },
98
+ },
99
+ },
100
+ },
101
+ },
88
102
  },
89
103
  },
90
104
  instructor_qualification_assignment: {
@@ -106,16 +120,21 @@ export class InstructorService {
106
120
 
107
121
  return {
108
122
  data: rows
109
- .map((row) => ({
110
- id: row.id,
111
- personId: row.person_id,
112
- name: row.person?.name?.trim() || `Instructor #${row.id}`,
113
- avatarId: row.person?.avatar_id ?? null,
114
- status: row.status,
115
- qualificationSlugs: row.instructor_qualification_assignment
116
- .map((assignment) => assignment.instructor_qualification.slug)
117
- .sort(),
118
- }))
123
+ .map((row) => {
124
+ const personUser = row.person?.person_user?.[0] ?? null;
125
+ return {
126
+ id: row.id,
127
+ personId: row.person_id,
128
+ name: row.person?.name?.trim() || `Instructor #${row.id}`,
129
+ avatarId: row.person?.avatar_id ?? null,
130
+ userId: personUser?.user_id ?? null,
131
+ hasTrainingAccess: (personUser?.user?.role_user?.length ?? 0) > 0,
132
+ status: row.status,
133
+ qualificationSlugs: row.instructor_qualification_assignment
134
+ .map((assignment) => assignment.instructor_qualification.slug)
135
+ .sort(),
136
+ };
137
+ })
119
138
  .sort((left, right) => left.name.localeCompare(right.name)),
120
139
  total,
121
140
  page,
@@ -269,6 +288,37 @@ export class InstructorService {
269
288
  });
270
289
  }
271
290
 
291
+ async getMe(userId: number) {
292
+ const instructor = await this.prisma.instructor.findFirst({
293
+ where: {
294
+ person: {
295
+ person_user: {
296
+ some: { user_id: userId },
297
+ },
298
+ },
299
+ },
300
+ select: {
301
+ id: true,
302
+ person: {
303
+ select: {
304
+ name: true,
305
+ avatar_id: true,
306
+ },
307
+ },
308
+ },
309
+ });
310
+
311
+ if (!instructor) {
312
+ return { isInstructor: false, avatarId: null, name: null };
313
+ }
314
+
315
+ return {
316
+ isInstructor: true,
317
+ avatarId: instructor.person?.avatar_id ?? null,
318
+ name: instructor.person?.name?.trim() || null,
319
+ };
320
+ }
321
+
272
322
  async getById(id: number, db: DbClient = this.prisma) {
273
323
  await this.ensureQualificationCatalog(db);
274
324
  await this.backfillLegacyCourseLessonQualifications(db);
@@ -295,6 +345,20 @@ export class InstructorService {
295
345
  },
296
346
  orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
297
347
  },
348
+ person_user: {
349
+ take: 1,
350
+ select: {
351
+ user_id: true,
352
+ user: {
353
+ select: {
354
+ role_user: {
355
+ where: { role: { slug: 'lms-instructor' } },
356
+ select: { id: true },
357
+ },
358
+ },
359
+ },
360
+ },
361
+ },
298
362
  },
299
363
  },
300
364
  instructor_qualification_assignment: {
@@ -313,11 +377,15 @@ export class InstructorService {
313
377
  throw new NotFoundException('Instructor not found');
314
378
  }
315
379
 
380
+ const personUser = instructor.person.person_user?.[0] ?? null;
381
+
316
382
  return {
317
383
  id: instructor.id,
318
384
  personId: instructor.person.id,
319
385
  name: instructor.person.name?.trim() || `Instructor #${instructor.id}`,
320
386
  avatarId: instructor.person.avatar_id ?? null,
387
+ userId: personUser?.user_id ?? null,
388
+ hasTrainingAccess: (personUser?.user?.role_user?.length ?? 0) > 0,
321
389
  email: this.getPrimaryContactValue(instructor.person.contact, ['EMAIL']),
322
390
  phone: this.getPrimaryContactValue(instructor.person.contact, [
323
391
  'PHONE',
@@ -331,6 +399,68 @@ export class InstructorService {
331
399
  };
332
400
  }
333
401
 
402
+ async setTrainingAccess(instructorId: number, enabled: boolean) {
403
+ const instructor = await this.prisma.instructor.findUnique({
404
+ where: { id: instructorId },
405
+ select: {
406
+ person: {
407
+ select: {
408
+ person_user: { take: 1, select: { user_id: true } },
409
+ },
410
+ },
411
+ },
412
+ });
413
+
414
+ const userId = instructor?.person?.person_user?.[0]?.user_id;
415
+ if (!userId) {
416
+ throw new BadRequestException('Instrutor não possui usuário vinculado.');
417
+ }
418
+
419
+ const role = await this.prisma.role.findFirst({
420
+ where: { slug: 'lms-instructor' },
421
+ select: { id: true },
422
+ });
423
+ if (!role) {
424
+ throw new BadRequestException('Role lms-instructor não encontrada.');
425
+ }
426
+
427
+ if (enabled) {
428
+ await this.prisma.role_user.upsert({
429
+ where: { role_id_user_id: { role_id: role.id, user_id: userId } },
430
+ create: { role_id: role.id, user_id: userId },
431
+ update: {},
432
+ });
433
+ } else {
434
+ await this.prisma.role_user.deleteMany({
435
+ where: { role_id: role.id, user_id: userId },
436
+ });
437
+ }
438
+
439
+ return { userId, enabled };
440
+ }
441
+
442
+ async getStats() {
443
+ const [total, active, inactive] = await Promise.all([
444
+ this.prisma.instructor.count(),
445
+ this.prisma.instructor.count({ where: { status: 'active' } }),
446
+ this.prisma.instructor.count({ where: { status: 'inactive' } }),
447
+ ]);
448
+ return { total, active, inactive };
449
+ }
450
+
451
+ async delete(id: number) {
452
+ const instructor = await this.prisma.instructor.findUnique({
453
+ where: { id },
454
+ select: { id: true },
455
+ });
456
+
457
+ if (!instructor) {
458
+ throw new NotFoundException('Instructor not found');
459
+ }
460
+
461
+ await this.prisma.instructor.delete({ where: { id } });
462
+ }
463
+
334
464
  async listQualifiedInstructorOptions(qualificationSlugs: string[]) {
335
465
  const result = await this.list({
336
466
  page: 1,