@hed-hog/lms 0.0.305 → 0.0.309
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/course/course-structure.controller.d.ts +60 -0
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +79 -0
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +61 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +326 -1
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +52 -4
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.service.d.ts +52 -5
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +81 -60
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -1
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +4 -1
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/move-lesson.dto.d.ts +10 -0
- package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
- package/dist/{enterprise/dto/add-enterprise-lead.dto.js → course/dto/move-lesson.dto.js} +12 -6
- package/dist/course/dto/move-lesson.dto.js.map +1 -0
- package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
- package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/paste-lessons.dto.js +24 -0
- package/dist/course/dto/paste-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.js +24 -0
- package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.js +24 -0
- package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
- package/dist/training/training.controller.js +1 -1
- package/dist/training/training.controller.js.map +1 -1
- package/hedhog/data/dashboard_component.yaml +152 -152
- package/hedhog/data/dashboard_item.yaml +166 -166
- package/hedhog/data/image_type.yaml +20 -0
- package/hedhog/data/menu.yaml +2 -2
- package/hedhog/data/route.yaml +60 -6
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
- package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
- package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
- package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +317 -317
- package/hedhog/frontend/app/enterprise/_components/enterprise-class-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +12 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-related-tab.tsx.ejs +44 -7
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +96 -96
- package/hedhog/frontend/app/page.tsx.ejs +5 -5
- package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
- package/hedhog/frontend/messages/en.json +88 -10
- package/hedhog/frontend/messages/pt.json +88 -10
- package/hedhog/table/course.yaml +16 -16
- package/hedhog/table/image_type.yaml +14 -0
- package/package.json +6 -6
- package/src/class-group/class-group.service.ts +413 -413
- package/src/class-group/dto/create-class-group.dto.ts +77 -77
- package/src/course/course-structure.controller.ts +63 -0
- package/src/course/course-structure.service.ts +390 -3
- package/src/course/course.service.ts +214 -182
- package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
- package/src/course/dto/create-course.dto.ts +19 -16
- package/src/course/dto/move-lesson.dto.ts +17 -0
- package/src/course/dto/paste-lessons.dto.ts +9 -0
- package/src/course/dto/reorder-lessons.dto.ts +10 -0
- package/src/course/dto/reorder-sessions.dto.ts +10 -0
- package/src/training/training.controller.ts +1 -1
- package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts +0 -4
- package/dist/enterprise/dto/add-enterprise-lead.dto.d.ts.map +0 -1
- package/dist/enterprise/dto/add-enterprise-lead.dto.js.map +0 -1
|
@@ -5,21 +5,21 @@ import { UpdateCourseDto } from './dto/update-course.dto';
|
|
|
5
5
|
|
|
6
6
|
type CourseImageTypeSlug = 'course-logo' | 'course-banner';
|
|
7
7
|
|
|
8
|
-
type CourseExtraFields = {
|
|
9
|
-
|
|
10
|
-
is_featured?: boolean | null;
|
|
11
|
-
has_certificate?: boolean | null;
|
|
12
|
-
is_listed?: boolean | null;
|
|
13
|
-
offering_type?: 'scheduled' | 'on_demand' | 'blended' | null;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type PersistCourseExtrasInput = {
|
|
17
|
-
|
|
18
|
-
isFeatured?: boolean;
|
|
19
|
-
hasCertificate?: boolean;
|
|
20
|
-
isListed?: boolean;
|
|
21
|
-
offeringType?: 'scheduled' | 'on_demand' | 'blended';
|
|
22
|
-
};
|
|
8
|
+
type CourseExtraFields = {
|
|
9
|
+
name?: string | null;
|
|
10
|
+
is_featured?: boolean | null;
|
|
11
|
+
has_certificate?: boolean | null;
|
|
12
|
+
is_listed?: boolean | null;
|
|
13
|
+
offering_type?: 'scheduled' | 'on_demand' | 'blended' | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type PersistCourseExtrasInput = {
|
|
17
|
+
name?: string;
|
|
18
|
+
isFeatured?: boolean;
|
|
19
|
+
hasCertificate?: boolean;
|
|
20
|
+
isListed?: boolean;
|
|
21
|
+
offeringType?: 'scheduled' | 'on_demand' | 'blended';
|
|
22
|
+
};
|
|
23
23
|
|
|
24
24
|
@Injectable()
|
|
25
25
|
export class CourseService {
|
|
@@ -40,9 +40,9 @@ export class CourseService {
|
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
private normalizeStatus(value?: string | null) {
|
|
44
|
-
if (!value) return undefined;
|
|
45
|
-
const normalized = String(value).trim().toLowerCase();
|
|
43
|
+
private normalizeStatus(value?: string | null) {
|
|
44
|
+
if (!value) return undefined;
|
|
45
|
+
const normalized = String(value).trim().toLowerCase();
|
|
46
46
|
if (
|
|
47
47
|
normalized === 'published' ||
|
|
48
48
|
normalized === 'active' ||
|
|
@@ -55,39 +55,39 @@ export class CourseService {
|
|
|
55
55
|
}
|
|
56
56
|
if (normalized === 'archived' || normalized === 'arquivado') {
|
|
57
57
|
return 'archived';
|
|
58
|
-
}
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private normalizeOfferingType(value?: string | null) {
|
|
63
|
-
if (!value) return undefined;
|
|
64
|
-
|
|
65
|
-
const normalized = String(value).trim().toLowerCase();
|
|
66
|
-
if (normalized === 'scheduled') {
|
|
67
|
-
return 'scheduled';
|
|
68
|
-
}
|
|
69
|
-
if (
|
|
70
|
-
normalized === 'on_demand' ||
|
|
71
|
-
normalized === 'ondemand' ||
|
|
72
|
-
normalized === 'on-demand'
|
|
73
|
-
) {
|
|
74
|
-
return 'on_demand';
|
|
75
|
-
}
|
|
76
|
-
if (normalized === 'blended') {
|
|
77
|
-
return 'blended';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private normalizeOptionalText(value?: string | null) {
|
|
84
|
-
const normalized = value?.trim();
|
|
85
|
-
return normalized ? normalized : undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private resolveCourseTitle(title: string | undefined, slug: string) {
|
|
89
|
-
return this.normalizeOptionalText(title) ?? slug;
|
|
90
|
-
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private normalizeOfferingType(value?: string | null) {
|
|
63
|
+
if (!value) return undefined;
|
|
64
|
+
|
|
65
|
+
const normalized = String(value).trim().toLowerCase();
|
|
66
|
+
if (normalized === 'scheduled') {
|
|
67
|
+
return 'scheduled';
|
|
68
|
+
}
|
|
69
|
+
if (
|
|
70
|
+
normalized === 'on_demand' ||
|
|
71
|
+
normalized === 'ondemand' ||
|
|
72
|
+
normalized === 'on-demand'
|
|
73
|
+
) {
|
|
74
|
+
return 'on_demand';
|
|
75
|
+
}
|
|
76
|
+
if (normalized === 'blended') {
|
|
77
|
+
return 'blended';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private normalizeOptionalText(value?: string | null) {
|
|
84
|
+
const normalized = value?.trim();
|
|
85
|
+
return normalized ? normalized : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private resolveCourseTitle(title: string | undefined, slug: string) {
|
|
89
|
+
return this.normalizeOptionalText(title) ?? slug;
|
|
90
|
+
}
|
|
91
91
|
|
|
92
92
|
async list(params: {
|
|
93
93
|
page?: number;
|
|
@@ -154,11 +154,11 @@ export class CourseService {
|
|
|
154
154
|
},
|
|
155
155
|
orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
|
|
156
156
|
},
|
|
157
|
-
_count: {
|
|
158
|
-
select: { course_enrollment: true, course_class_group: true },
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
}),
|
|
157
|
+
_count: {
|
|
158
|
+
select: { course_enrollment: true, course_class_group: true },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
162
|
this.prisma.course.count({ where }),
|
|
163
163
|
]);
|
|
164
164
|
|
|
@@ -209,7 +209,13 @@ export class CourseService {
|
|
|
209
209
|
},
|
|
210
210
|
include: {
|
|
211
211
|
image_type: {
|
|
212
|
-
select: {
|
|
212
|
+
select: {
|
|
213
|
+
slug: true,
|
|
214
|
+
suggested_width: true,
|
|
215
|
+
suggested_height: true,
|
|
216
|
+
allowed_extensions: true,
|
|
217
|
+
aspect_ratio: true,
|
|
218
|
+
},
|
|
213
219
|
},
|
|
214
220
|
file: {
|
|
215
221
|
select: { id: true, filename: true },
|
|
@@ -306,14 +312,13 @@ export class CourseService {
|
|
|
306
312
|
}, extrasById.get(id));
|
|
307
313
|
}
|
|
308
314
|
|
|
309
|
-
async create(dto: CreateCourseDto) {
|
|
310
|
-
const { categorySlugs, logoFileId, bannerFileId, instructorIds, ...data } = dto;
|
|
311
|
-
const normalizedSlug = data.slug.trim();
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
? await this.prisma.category.findMany({
|
|
315
|
+
async create(dto: CreateCourseDto) {
|
|
316
|
+
const { categorySlugs, logoFileId, bannerFileId, instructorIds, ...data } = dto;
|
|
317
|
+
const normalizedSlug = data.slug.trim();
|
|
318
|
+
const resolvedTitle = this.resolveCourseTitle(data.title, normalizedSlug);
|
|
319
|
+
|
|
320
|
+
const categories = categorySlugs?.length
|
|
321
|
+
? await this.prisma.category.findMany({
|
|
317
322
|
where: { slug: { in: categorySlugs } },
|
|
318
323
|
select: { id: true },
|
|
319
324
|
})
|
|
@@ -330,15 +335,16 @@ export class CourseService {
|
|
|
330
335
|
validInstructorIds = instructorIds.filter((id) => validIds.has(id));
|
|
331
336
|
}
|
|
332
337
|
|
|
333
|
-
const c = await this.prisma.course.create({
|
|
334
|
-
data: {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
const c = await this.prisma.course.create({
|
|
339
|
+
data: {
|
|
340
|
+
name: data.name,
|
|
341
|
+
slug: normalizedSlug,
|
|
342
|
+
title: resolvedTitle,
|
|
343
|
+
...(data.description !== undefined && {
|
|
344
|
+
description: data.description,
|
|
345
|
+
}),
|
|
346
|
+
level: this.normalizeLevel(data.level) ?? 'beginner',
|
|
347
|
+
status: this.normalizeStatus(data.status) ?? 'draft',
|
|
342
348
|
...(data.requirements !== undefined && {
|
|
343
349
|
requirements: data.requirements,
|
|
344
350
|
}),
|
|
@@ -425,42 +431,41 @@ export class CourseService {
|
|
|
425
431
|
bannerFileId,
|
|
426
432
|
});
|
|
427
433
|
|
|
428
|
-
await this.persistCourseExtras(c.id, {
|
|
429
|
-
...data,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
});
|
|
434
|
+
await this.persistCourseExtras(c.id, {
|
|
435
|
+
...data,
|
|
436
|
+
offeringType: this.normalizeOfferingType(data.offeringType) ?? 'on_demand',
|
|
437
|
+
});
|
|
433
438
|
|
|
434
439
|
const extrasById = await this.getCourseExtras([c.id]);
|
|
435
440
|
|
|
436
441
|
return this.mapCourse(c, undefined, extrasById.get(c.id));
|
|
437
442
|
}
|
|
438
443
|
|
|
439
|
-
async update(id: number, dto: UpdateCourseDto) {
|
|
440
|
-
const { categorySlugs, logoFileId, bannerFileId, instructorIds, ...data } = dto;
|
|
441
|
-
let existingSlug: string | undefined;
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
data.title !== undefined &&
|
|
445
|
-
this.normalizeOptionalText(data.title) === undefined &&
|
|
446
|
-
data.slug === undefined
|
|
447
|
-
) {
|
|
448
|
-
const currentCourse = await this.prisma.course.findUnique({
|
|
449
|
-
where: { id },
|
|
450
|
-
select: { slug: true },
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
existingSlug = currentCourse?.slug;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const resolvedSlug = data.slug !== undefined ? data.slug.trim() : undefined;
|
|
457
|
-
const resolvedTitle =
|
|
458
|
-
data.title !== undefined
|
|
459
|
-
? this.resolveCourseTitle(data.title, resolvedSlug ?? existingSlug ?? '')
|
|
460
|
-
: undefined;
|
|
461
|
-
|
|
462
|
-
const categories = categorySlugs?.length
|
|
463
|
-
? await this.prisma.category.findMany({
|
|
444
|
+
async update(id: number, dto: UpdateCourseDto) {
|
|
445
|
+
const { categorySlugs, logoFileId, bannerFileId, instructorIds, ...data } = dto;
|
|
446
|
+
let existingSlug: string | undefined;
|
|
447
|
+
|
|
448
|
+
if (
|
|
449
|
+
data.title !== undefined &&
|
|
450
|
+
this.normalizeOptionalText(data.title) === undefined &&
|
|
451
|
+
data.slug === undefined
|
|
452
|
+
) {
|
|
453
|
+
const currentCourse = await this.prisma.course.findUnique({
|
|
454
|
+
where: { id },
|
|
455
|
+
select: { slug: true },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
existingSlug = currentCourse?.slug;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const resolvedSlug = data.slug !== undefined ? data.slug.trim() : undefined;
|
|
462
|
+
const resolvedTitle =
|
|
463
|
+
data.title !== undefined
|
|
464
|
+
? this.resolveCourseTitle(data.title, resolvedSlug ?? existingSlug ?? '')
|
|
465
|
+
: undefined;
|
|
466
|
+
|
|
467
|
+
const categories = categorySlugs?.length
|
|
468
|
+
? await this.prisma.category.findMany({
|
|
464
469
|
where: { slug: { in: categorySlugs } },
|
|
465
470
|
select: { id: true },
|
|
466
471
|
})
|
|
@@ -487,14 +492,15 @@ export class CourseService {
|
|
|
487
492
|
});
|
|
488
493
|
}
|
|
489
494
|
|
|
490
|
-
const c = await this.prisma.course.update({
|
|
491
|
-
where: { id },
|
|
492
|
-
data: {
|
|
493
|
-
...(
|
|
494
|
-
...(
|
|
495
|
-
...(
|
|
496
|
-
...(data.
|
|
497
|
-
|
|
495
|
+
const c = await this.prisma.course.update({
|
|
496
|
+
where: { id },
|
|
497
|
+
data: {
|
|
498
|
+
...(data.name !== undefined && { name: data.name }),
|
|
499
|
+
...(resolvedSlug !== undefined && { slug: resolvedSlug }),
|
|
500
|
+
...(resolvedTitle !== undefined && { title: resolvedTitle }),
|
|
501
|
+
...(data.description !== undefined && { description: data.description }),
|
|
502
|
+
...(data.level !== undefined && {
|
|
503
|
+
level: this.normalizeLevel(data.level),
|
|
498
504
|
}),
|
|
499
505
|
...(data.status !== undefined && {
|
|
500
506
|
status: this.normalizeStatus(data.status),
|
|
@@ -594,7 +600,7 @@ export class CourseService {
|
|
|
594
600
|
bannerFileId,
|
|
595
601
|
});
|
|
596
602
|
|
|
597
|
-
await this.persistCourseExtras(id, data);
|
|
603
|
+
await this.persistCourseExtras(id, data);
|
|
598
604
|
|
|
599
605
|
const extrasById = await this.getCourseExtras([id]);
|
|
600
606
|
|
|
@@ -606,7 +612,7 @@ export class CourseService {
|
|
|
606
612
|
return { success: true };
|
|
607
613
|
}
|
|
608
614
|
|
|
609
|
-
private mapCourse(
|
|
615
|
+
private mapCourse(
|
|
610
616
|
c: any,
|
|
611
617
|
metrics?: {
|
|
612
618
|
lessonCount?: number;
|
|
@@ -620,16 +626,16 @@ export class CourseService {
|
|
|
620
626
|
}[];
|
|
621
627
|
},
|
|
622
628
|
extras?: CourseExtraFields,
|
|
623
|
-
) {
|
|
624
|
-
const
|
|
625
|
-
const resolvedTitle = this.normalizeOptionalText(c.title) ?? c.slug;
|
|
626
|
-
const courseImages = Array.isArray(c.course_image) ? c.course_image : [];
|
|
627
|
-
const classCount = c._count?.course_class_group ?? 0;
|
|
628
|
-
const offeringType =
|
|
629
|
-
this.normalizeOfferingType(extras?.offering_type) ??
|
|
630
|
-
(classCount > 0 ? 'scheduled' : 'on_demand');
|
|
631
|
-
|
|
632
|
-
const logoImage = courseImages.find(
|
|
629
|
+
) {
|
|
630
|
+
const resolvedName = extras?.name ?? c.name ?? null;
|
|
631
|
+
const resolvedTitle = this.normalizeOptionalText(c.title) ?? c.slug;
|
|
632
|
+
const courseImages = Array.isArray(c.course_image) ? c.course_image : [];
|
|
633
|
+
const classCount = c._count?.course_class_group ?? 0;
|
|
634
|
+
const offeringType =
|
|
635
|
+
this.normalizeOfferingType(extras?.offering_type) ??
|
|
636
|
+
(classCount > 0 ? 'scheduled' : 'on_demand');
|
|
637
|
+
|
|
638
|
+
const logoImage = courseImages.find(
|
|
633
639
|
(courseImage: any) => courseImage.image_type?.slug === 'course-logo',
|
|
634
640
|
);
|
|
635
641
|
const bannerImage = courseImages.find(
|
|
@@ -638,12 +644,10 @@ export class CourseService {
|
|
|
638
644
|
|
|
639
645
|
return {
|
|
640
646
|
id: c.id,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
title: resolvedTitle,
|
|
646
|
-
description: c.description ?? '',
|
|
647
|
+
name: resolvedName ?? '',
|
|
648
|
+
slug: c.slug,
|
|
649
|
+
title: resolvedTitle,
|
|
650
|
+
description: c.description ?? '',
|
|
647
651
|
level: c.level,
|
|
648
652
|
status: c.status,
|
|
649
653
|
durationHours: c.duration_hours ?? 0,
|
|
@@ -657,8 +661,24 @@ export class CourseService {
|
|
|
657
661
|
secondaryContrastColor: c.secondary_contrast_color ?? null,
|
|
658
662
|
logoFileId: logoImage?.file?.id ?? logoImage?.file_id ?? null,
|
|
659
663
|
logoFilename: logoImage?.file?.filename ?? null,
|
|
664
|
+
logoImageType: logoImage?.image_type
|
|
665
|
+
? {
|
|
666
|
+
suggestedWidth: logoImage.image_type.suggested_width ?? null,
|
|
667
|
+
suggestedHeight: logoImage.image_type.suggested_height ?? null,
|
|
668
|
+
allowedExtensions: logoImage.image_type.allowed_extensions ?? null,
|
|
669
|
+
aspectRatio: logoImage.image_type.aspect_ratio ?? null,
|
|
670
|
+
}
|
|
671
|
+
: null,
|
|
660
672
|
bannerFileId: bannerImage?.file?.id ?? bannerImage?.file_id ?? null,
|
|
661
673
|
bannerFilename: bannerImage?.file?.filename ?? null,
|
|
674
|
+
bannerImageType: bannerImage?.image_type
|
|
675
|
+
? {
|
|
676
|
+
suggestedWidth: bannerImage.image_type.suggested_width ?? null,
|
|
677
|
+
suggestedHeight: bannerImage.image_type.suggested_height ?? null,
|
|
678
|
+
allowedExtensions: bannerImage.image_type.allowed_extensions ?? null,
|
|
679
|
+
aspectRatio: bannerImage.image_type.aspect_ratio ?? null,
|
|
680
|
+
}
|
|
681
|
+
: null,
|
|
662
682
|
categories: (c.course_category ?? []).map(
|
|
663
683
|
(cc: any) => cc.category?.slug ?? '',
|
|
664
684
|
),
|
|
@@ -666,14 +686,14 @@ export class CourseService {
|
|
|
666
686
|
(cc: any) => cc.category?.id ?? 0,
|
|
667
687
|
),
|
|
668
688
|
isFeatured: extras?.is_featured ?? c.is_featured ?? false,
|
|
669
|
-
hasCertificate: extras?.has_certificate ?? c.has_certificate ?? false,
|
|
670
|
-
isListed: extras?.is_listed ?? c.is_listed ?? false,
|
|
671
|
-
offeringType,
|
|
672
|
-
enrollmentCount: c._count?.course_enrollment ?? 0,
|
|
673
|
-
moduleCount: c._count?.course_module ?? 0,
|
|
674
|
-
classCount,
|
|
675
|
-
lessonCount: metrics?.lessonCount ?? 0,
|
|
676
|
-
sessionCount: metrics?.sessionCount ?? 0,
|
|
689
|
+
hasCertificate: extras?.has_certificate ?? c.has_certificate ?? false,
|
|
690
|
+
isListed: extras?.is_listed ?? c.is_listed ?? false,
|
|
691
|
+
offeringType,
|
|
692
|
+
enrollmentCount: c._count?.course_enrollment ?? 0,
|
|
693
|
+
moduleCount: c._count?.course_module ?? 0,
|
|
694
|
+
classCount,
|
|
695
|
+
lessonCount: metrics?.lessonCount ?? 0,
|
|
696
|
+
sessionCount: metrics?.sessionCount ?? 0,
|
|
677
697
|
averageCompletion: Math.round(metrics?.averageCompletion ?? 0),
|
|
678
698
|
certificatesIssued:
|
|
679
699
|
metrics?.certificatesIssued ?? c._count?.certificate ?? 0,
|
|
@@ -720,29 +740,29 @@ export class CourseService {
|
|
|
720
740
|
try {
|
|
721
741
|
const idsCsv = normalizedIds.join(',');
|
|
722
742
|
const rows = (await this.prisma.$queryRawUnsafe(
|
|
723
|
-
`
|
|
724
|
-
SELECT id,
|
|
725
|
-
FROM course
|
|
726
|
-
WHERE id IN (${idsCsv})
|
|
727
|
-
`,
|
|
743
|
+
`
|
|
744
|
+
SELECT id, name, is_featured, has_certificate, is_listed, offering_type
|
|
745
|
+
FROM course
|
|
746
|
+
WHERE id IN (${idsCsv})
|
|
747
|
+
`,
|
|
728
748
|
)) as Array<{
|
|
729
749
|
id: number;
|
|
730
|
-
|
|
750
|
+
name: string | null;
|
|
731
751
|
is_featured: boolean | null;
|
|
732
|
-
has_certificate: boolean | null;
|
|
733
|
-
is_listed: boolean | null;
|
|
734
|
-
offering_type: 'scheduled' | 'on_demand' | 'blended' | null;
|
|
735
|
-
}>;
|
|
736
|
-
|
|
737
|
-
for (const row of rows) {
|
|
738
|
-
extrasById.set(row.id, {
|
|
739
|
-
|
|
740
|
-
is_featured: row.is_featured,
|
|
741
|
-
has_certificate: row.has_certificate,
|
|
742
|
-
is_listed: row.is_listed,
|
|
743
|
-
offering_type: row.offering_type,
|
|
744
|
-
});
|
|
745
|
-
}
|
|
752
|
+
has_certificate: boolean | null;
|
|
753
|
+
is_listed: boolean | null;
|
|
754
|
+
offering_type: 'scheduled' | 'on_demand' | 'blended' | null;
|
|
755
|
+
}>;
|
|
756
|
+
|
|
757
|
+
for (const row of rows) {
|
|
758
|
+
extrasById.set(row.id, {
|
|
759
|
+
name: row.name,
|
|
760
|
+
is_featured: row.is_featured,
|
|
761
|
+
has_certificate: row.has_certificate,
|
|
762
|
+
is_listed: row.is_listed,
|
|
763
|
+
offering_type: row.offering_type,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
746
766
|
} catch {
|
|
747
767
|
return extrasById;
|
|
748
768
|
}
|
|
@@ -757,9 +777,9 @@ export class CourseService {
|
|
|
757
777
|
const sets: string[] = [];
|
|
758
778
|
const params: Array<string | boolean | number> = [];
|
|
759
779
|
|
|
760
|
-
if (data.
|
|
761
|
-
params.push(data.
|
|
762
|
-
sets.push(`
|
|
780
|
+
if (data.name !== undefined) {
|
|
781
|
+
params.push(data.name);
|
|
782
|
+
sets.push(`name = $${params.length}`);
|
|
763
783
|
}
|
|
764
784
|
|
|
765
785
|
if (data.isFeatured !== undefined) {
|
|
@@ -772,15 +792,15 @@ export class CourseService {
|
|
|
772
792
|
sets.push(`has_certificate = $${params.length}`);
|
|
773
793
|
}
|
|
774
794
|
|
|
775
|
-
if (data.isListed !== undefined) {
|
|
776
|
-
params.push(data.isListed);
|
|
777
|
-
sets.push(`is_listed = $${params.length}`);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (data.offeringType !== undefined) {
|
|
781
|
-
params.push(data.offeringType);
|
|
782
|
-
sets.push(`offering_type = $${params.length}`);
|
|
783
|
-
}
|
|
795
|
+
if (data.isListed !== undefined) {
|
|
796
|
+
params.push(data.isListed);
|
|
797
|
+
sets.push(`is_listed = $${params.length}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (data.offeringType !== undefined) {
|
|
801
|
+
params.push(data.offeringType);
|
|
802
|
+
sets.push(`offering_type = $${params.length}`);
|
|
803
|
+
}
|
|
784
804
|
|
|
785
805
|
if (sets.length === 0) {
|
|
786
806
|
return;
|
|
@@ -802,18 +822,6 @@ export class CourseService {
|
|
|
802
822
|
}
|
|
803
823
|
}
|
|
804
824
|
|
|
805
|
-
private normalizeCourseCode(value?: string | null) {
|
|
806
|
-
const normalized = String(value ?? '')
|
|
807
|
-
.trim()
|
|
808
|
-
.toUpperCase()
|
|
809
|
-
.replace(/[^A-Z0-9-]+/g, '-')
|
|
810
|
-
.replace(/-+/g, '-')
|
|
811
|
-
.replace(/^-+|-+$/g, '')
|
|
812
|
-
.slice(0, 32);
|
|
813
|
-
|
|
814
|
-
return normalized || undefined;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
825
|
private async syncCourseImages(
|
|
818
826
|
courseId: number,
|
|
819
827
|
data: {
|
|
@@ -908,11 +916,35 @@ export class CourseService {
|
|
|
908
916
|
return existing;
|
|
909
917
|
}
|
|
910
918
|
|
|
919
|
+
const specsBySlug: Record<
|
|
920
|
+
CourseImageTypeSlug,
|
|
921
|
+
{
|
|
922
|
+
suggested_width: number;
|
|
923
|
+
suggested_height: number;
|
|
924
|
+
allowed_extensions: string;
|
|
925
|
+
aspect_ratio: string;
|
|
926
|
+
}
|
|
927
|
+
> = {
|
|
928
|
+
'course-logo': {
|
|
929
|
+
suggested_width: 500,
|
|
930
|
+
suggested_height: 500,
|
|
931
|
+
allowed_extensions: 'png,webp',
|
|
932
|
+
aspect_ratio: '1:1',
|
|
933
|
+
},
|
|
934
|
+
'course-banner': {
|
|
935
|
+
suggested_width: 1920,
|
|
936
|
+
suggested_height: 540,
|
|
937
|
+
allowed_extensions: 'jpg,png,webp',
|
|
938
|
+
aspect_ratio: '16:9',
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
|
|
911
942
|
return this.prisma.image_type.create({
|
|
912
943
|
data: {
|
|
913
944
|
name: imageTypeSlug === 'course-logo' ? 'Course Logo' : 'Course Banner',
|
|
914
945
|
slug: imageTypeSlug,
|
|
915
946
|
applies_to_course: true,
|
|
947
|
+
...specsBySlug[imageTypeSlug],
|
|
916
948
|
},
|
|
917
949
|
select: { id: true },
|
|
918
950
|
});
|
|
@@ -56,8 +56,9 @@ export class CreateCourseStructureLessonDto {
|
|
|
56
56
|
tipo: 'video' | 'questao' | 'post' | 'exercicio';
|
|
57
57
|
|
|
58
58
|
@IsInt()
|
|
59
|
-
@Min(
|
|
60
|
-
|
|
59
|
+
@Min(0)
|
|
60
|
+
@IsOptional()
|
|
61
|
+
duracao: number = 0;
|
|
61
62
|
|
|
62
63
|
@IsEnum(['youtube', 'vimeo', 'bunny', 'custom'])
|
|
63
64
|
@IsOptional()
|
|
@@ -14,17 +14,20 @@ export class CreateCourseDto {
|
|
|
14
14
|
@IsString()
|
|
15
15
|
@MaxLength(32)
|
|
16
16
|
@IsOptional()
|
|
17
|
-
|
|
17
|
+
name?: string;
|
|
18
18
|
|
|
19
19
|
@IsString()
|
|
20
20
|
@IsNotEmpty()
|
|
21
21
|
@MaxLength(255)
|
|
22
|
+
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
|
|
23
|
+
message: 'slug deve conter apenas letras minúsculas, números e hífens, sem espaços',
|
|
24
|
+
})
|
|
22
25
|
slug: string;
|
|
23
26
|
|
|
24
|
-
@IsString()
|
|
25
|
-
@MaxLength(255)
|
|
26
|
-
@IsOptional()
|
|
27
|
-
title?: string;
|
|
27
|
+
@IsString()
|
|
28
|
+
@MaxLength(255)
|
|
29
|
+
@IsOptional()
|
|
30
|
+
title?: string;
|
|
28
31
|
|
|
29
32
|
@IsString()
|
|
30
33
|
@IsOptional()
|
|
@@ -52,17 +55,17 @@ export class CreateCourseDto {
|
|
|
52
55
|
@IsOptional()
|
|
53
56
|
level?: 'beginner' | 'intermediate' | 'advanced';
|
|
54
57
|
|
|
55
|
-
@IsEnum(['draft', 'published', 'archived'])
|
|
56
|
-
@IsOptional()
|
|
57
|
-
status?: 'draft' | 'published' | 'archived';
|
|
58
|
-
|
|
59
|
-
@IsEnum(['scheduled', 'on_demand', 'blended'])
|
|
60
|
-
@IsOptional()
|
|
61
|
-
offeringType?: 'scheduled' | 'on_demand' | 'blended';
|
|
62
|
-
|
|
63
|
-
@IsBoolean()
|
|
64
|
-
@IsOptional()
|
|
65
|
-
isFeatured?: boolean;
|
|
58
|
+
@IsEnum(['draft', 'published', 'archived'])
|
|
59
|
+
@IsOptional()
|
|
60
|
+
status?: 'draft' | 'published' | 'archived';
|
|
61
|
+
|
|
62
|
+
@IsEnum(['scheduled', 'on_demand', 'blended'])
|
|
63
|
+
@IsOptional()
|
|
64
|
+
offeringType?: 'scheduled' | 'on_demand' | 'blended';
|
|
65
|
+
|
|
66
|
+
@IsBoolean()
|
|
67
|
+
@IsOptional()
|
|
68
|
+
isFeatured?: boolean;
|
|
66
69
|
|
|
67
70
|
@IsBoolean()
|
|
68
71
|
@IsOptional()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { IsInt, IsOptional, IsPositive, Min } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class MoveLessonDto {
|
|
4
|
+
/** ID of the destination session (course_module). */
|
|
5
|
+
@IsInt()
|
|
6
|
+
@IsPositive()
|
|
7
|
+
toSessionId: number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Zero-based index within the destination session.
|
|
11
|
+
* If omitted, the lesson is appended at the end.
|
|
12
|
+
*/
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsInt()
|
|
15
|
+
@Min(0)
|
|
16
|
+
toIndex?: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ArrayMinSize, IsArray, IsInt, IsPositive } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ReorderLessonsDto {
|
|
4
|
+
/** Ordered array of lesson IDs in their new positions within the session. */
|
|
5
|
+
@IsArray()
|
|
6
|
+
@ArrayMinSize(1)
|
|
7
|
+
@IsInt({ each: true })
|
|
8
|
+
@IsPositive({ each: true })
|
|
9
|
+
lessonIds: number[];
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ArrayMinSize, IsArray, IsInt, IsPositive } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ReorderSessionsDto {
|
|
4
|
+
/** Ordered array of session IDs reflecting the new visual order. */
|
|
5
|
+
@IsArray()
|
|
6
|
+
@ArrayMinSize(1)
|
|
7
|
+
@IsInt({ each: true })
|
|
8
|
+
@IsPositive({ each: true })
|
|
9
|
+
sessionIds: number[];
|
|
10
|
+
}
|
|
@@ -15,7 +15,7 @@ import { UpdateTrainingDto } from './dto/update-training.dto';
|
|
|
15
15
|
import { TrainingService } from './training.service';
|
|
16
16
|
|
|
17
17
|
@Role()
|
|
18
|
-
@Controller('lms/
|
|
18
|
+
@Controller('lms/paths')
|
|
19
19
|
export class TrainingController {
|
|
20
20
|
constructor(private readonly trainingService: TrainingService) {}
|
|
21
21
|
|