@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.
- package/dist/class-group/class-group.controller.d.ts +2 -2
- package/dist/class-group/class-group.service.d.ts +2 -2
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +3 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +14 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +128 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +23 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +41 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +126 -8
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/data/menu.yaml +23 -7
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +8 -8
- package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
- package/src/enterprise/enterprise.controller.ts +9 -1
- package/src/enterprise/enterprise.service.ts +147 -4
- package/src/instructor/instructor.controller.ts +36 -9
- package/src/instructor/instructor.service.ts +140 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/lms",
|
|
3
|
-
"version": "0.0.
|
|
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
|
|
13
|
-
"@hed-hog/
|
|
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/
|
|
19
|
-
"@hed-hog/
|
|
20
|
-
"@hed-hog/category": "0.0.
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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,
|