@hed-hog/lms 0.0.306 → 0.0.310

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 (120) hide show
  1. package/dist/course/course-structure.controller.d.ts +60 -0
  2. package/dist/course/course-structure.controller.d.ts.map +1 -1
  3. package/dist/course/course-structure.controller.js +79 -0
  4. package/dist/course/course-structure.controller.js.map +1 -1
  5. package/dist/course/course-structure.service.d.ts +61 -1
  6. package/dist/course/course-structure.service.d.ts.map +1 -1
  7. package/dist/course/course-structure.service.js +326 -1
  8. package/dist/course/course-structure.service.js.map +1 -1
  9. package/dist/course/course.controller.d.ts +52 -4
  10. package/dist/course/course.controller.d.ts.map +1 -1
  11. package/dist/course/course.service.d.ts +52 -5
  12. package/dist/course/course.service.d.ts.map +1 -1
  13. package/dist/course/course.service.js +78 -57
  14. package/dist/course/course.service.js.map +1 -1
  15. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  16. package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
  17. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  18. package/dist/course/dto/create-course.dto.d.ts +1 -1
  19. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  20. package/dist/course/dto/create-course.dto.js +4 -1
  21. package/dist/course/dto/create-course.dto.js.map +1 -1
  22. package/dist/course/dto/move-lesson.dto.d.ts +10 -0
  23. package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
  24. package/dist/course/dto/move-lesson.dto.js +28 -0
  25. package/dist/course/dto/move-lesson.dto.js.map +1 -0
  26. package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
  27. package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
  28. package/dist/course/dto/paste-lessons.dto.js +24 -0
  29. package/dist/course/dto/paste-lessons.dto.js.map +1 -0
  30. package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
  31. package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
  32. package/dist/course/dto/reorder-lessons.dto.js +24 -0
  33. package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
  34. package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
  35. package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
  36. package/dist/course/dto/reorder-sessions.dto.js +24 -0
  37. package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
  38. package/dist/training/training.controller.js +1 -1
  39. package/dist/training/training.controller.js.map +1 -1
  40. package/hedhog/data/image_type.yaml +20 -0
  41. package/hedhog/data/menu.yaml +2 -2
  42. package/hedhog/data/route.yaml +60 -6
  43. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
  44. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
  45. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
  46. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +10 -1
  47. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -3
  48. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +32 -0
  49. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
  50. package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
  51. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
  52. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
  53. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
  54. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
  55. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
  58. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
  59. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
  60. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
  61. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  62. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
  97. package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
  98. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
  99. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
  100. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
  101. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
  102. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
  104. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
  105. package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
  106. package/hedhog/frontend/messages/en.json +91 -11
  107. package/hedhog/frontend/messages/pt.json +91 -11
  108. package/hedhog/table/course.yaml +1 -1
  109. package/hedhog/table/image_type.yaml +14 -0
  110. package/package.json +7 -7
  111. package/src/course/course-structure.controller.ts +63 -0
  112. package/src/course/course-structure.service.ts +390 -3
  113. package/src/course/course.service.ts +59 -27
  114. package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
  115. package/src/course/dto/create-course.dto.ts +4 -1
  116. package/src/course/dto/move-lesson.dto.ts +17 -0
  117. package/src/course/dto/paste-lessons.dto.ts +9 -0
  118. package/src/course/dto/reorder-lessons.dto.ts +10 -0
  119. package/src/course/dto/reorder-sessions.dto.ts +10 -0
  120. package/src/training/training.controller.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/lms",
3
- "version": "0.0.306",
3
+ "version": "0.0.310",
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-types": "0.0.1",
12
13
  "@hed-hog/api-prisma": "0.0.6",
13
- "@hed-hog/contact": "0.0.306",
14
14
  "@hed-hog/api-pagination": "0.0.7",
15
- "@hed-hog/core": "0.0.306",
15
+ "@hed-hog/api": "0.0.6",
16
+ "@hed-hog/contact": "0.0.310",
16
17
  "@hed-hog/api-locale": "0.0.14",
17
- "@hed-hog/api-types": "0.0.1",
18
- "@hed-hog/finance": "0.0.306",
19
- "@hed-hog/category": "0.0.306",
20
- "@hed-hog/api": "0.0.6"
18
+ "@hed-hog/finance": "0.0.310",
19
+ "@hed-hog/core": "0.0.310",
20
+ "@hed-hog/category": "0.0.310"
21
21
  },
22
22
  "exports": {
23
23
  ".": {
@@ -12,6 +12,10 @@ import {
12
12
  import { CourseStructureService } from './course-structure.service';
13
13
  import { CreateCourseStructureLessonDto } from './dto/create-course-structure-lesson.dto';
14
14
  import { CreateCourseStructureSessionDto } from './dto/create-course-structure-session.dto';
15
+ import { MoveLessonDto } from './dto/move-lesson.dto';
16
+ import { PasteLessonsDto } from './dto/paste-lessons.dto';
17
+ import { ReorderLessonsDto } from './dto/reorder-lessons.dto';
18
+ import { ReorderSessionsDto } from './dto/reorder-sessions.dto';
15
19
  import { UpdateCourseStructureLessonDto } from './dto/update-course-structure-lesson.dto';
16
20
  import { UpdateCourseStructureSessionDto } from './dto/update-course-structure-session.dto';
17
21
 
@@ -25,6 +29,8 @@ export class CourseStructureController {
25
29
  return this.courseStructureService.getStructure(courseId);
26
30
  }
27
31
 
32
+ // ── Sessions ──────────────────────────────────────────────────────────────
33
+
28
34
  @Post('sessions')
29
35
  createSession(
30
36
  @Param('id', ParseIntPipe) courseId: number,
@@ -33,6 +39,15 @@ export class CourseStructureController {
33
39
  return this.courseStructureService.createSession(courseId, dto);
34
40
  }
35
41
 
42
+ // Static PATCH must come before dynamic PATCH sessions/:sessionId
43
+ @Patch('sessions/reorder')
44
+ reorderSessions(
45
+ @Param('id', ParseIntPipe) courseId: number,
46
+ @Body() dto: ReorderSessionsDto,
47
+ ) {
48
+ return this.courseStructureService.reorderSessions(courseId, dto);
49
+ }
50
+
36
51
  @Patch('sessions/:sessionId')
37
52
  updateSession(
38
53
  @Param('id', ParseIntPipe) courseId: number,
@@ -50,6 +65,16 @@ export class CourseStructureController {
50
65
  return this.courseStructureService.deleteSession(courseId, sessionId);
51
66
  }
52
67
 
68
+ @Post('sessions/:sessionId/duplicate')
69
+ duplicateSession(
70
+ @Param('id', ParseIntPipe) courseId: number,
71
+ @Param('sessionId', ParseIntPipe) sessionId: number,
72
+ ) {
73
+ return this.courseStructureService.duplicateSession(courseId, sessionId);
74
+ }
75
+
76
+ // ── Lessons ───────────────────────────────────────────────────────────────
77
+
53
78
  @Post('sessions/:sessionId/lessons')
54
79
  createLesson(
55
80
  @Param('id', ParseIntPipe) courseId: number,
@@ -59,6 +84,25 @@ export class CourseStructureController {
59
84
  return this.courseStructureService.createLesson(courseId, sessionId, dto);
60
85
  }
61
86
 
87
+ @Post('sessions/:sessionId/lessons/paste')
88
+ pasteLessons(
89
+ @Param('id', ParseIntPipe) courseId: number,
90
+ @Param('sessionId', ParseIntPipe) sessionId: number,
91
+ @Body() dto: PasteLessonsDto,
92
+ ) {
93
+ return this.courseStructureService.pasteLessons(courseId, sessionId, dto);
94
+ }
95
+
96
+ // Static PATCH must come before dynamic PATCH sessions/:sessionId/lessons/:lessonId
97
+ @Patch('sessions/:sessionId/lessons/reorder')
98
+ reorderLessons(
99
+ @Param('id', ParseIntPipe) courseId: number,
100
+ @Param('sessionId', ParseIntPipe) sessionId: number,
101
+ @Body() dto: ReorderLessonsDto,
102
+ ) {
103
+ return this.courseStructureService.reorderLessons(courseId, sessionId, dto);
104
+ }
105
+
62
106
  @Patch('sessions/:sessionId/lessons/:lessonId')
63
107
  updateLesson(
64
108
  @Param('id', ParseIntPipe) courseId: number,
@@ -74,6 +118,16 @@ export class CourseStructureController {
74
118
  );
75
119
  }
76
120
 
121
+ @Patch('sessions/:sessionId/lessons/:lessonId/move')
122
+ moveLesson(
123
+ @Param('id', ParseIntPipe) courseId: number,
124
+ @Param('sessionId', ParseIntPipe) sessionId: number,
125
+ @Param('lessonId', ParseIntPipe) lessonId: number,
126
+ @Body() dto: MoveLessonDto,
127
+ ) {
128
+ return this.courseStructureService.moveLesson(courseId, sessionId, lessonId, dto);
129
+ }
130
+
77
131
  @Delete('sessions/:sessionId/lessons/:lessonId')
78
132
  deleteLesson(
79
133
  @Param('id', ParseIntPipe) courseId: number,
@@ -82,4 +136,13 @@ export class CourseStructureController {
82
136
  ) {
83
137
  return this.courseStructureService.deleteLesson(courseId, sessionId, lessonId);
84
138
  }
139
+
140
+ @Post('sessions/:sessionId/lessons/:lessonId/duplicate')
141
+ duplicateLesson(
142
+ @Param('id', ParseIntPipe) courseId: number,
143
+ @Param('sessionId', ParseIntPipe) sessionId: number,
144
+ @Param('lessonId', ParseIntPipe) lessonId: number,
145
+ ) {
146
+ return this.courseStructureService.duplicateLesson(courseId, sessionId, lessonId);
147
+ }
85
148
  }
@@ -1,10 +1,14 @@
1
1
  import { PrismaService } from '@hed-hog/api-prisma';
2
2
  import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
3
+ import { InstructorService } from '../instructor/instructor.service';
3
4
  import { CreateCourseStructureLessonDto } from './dto/create-course-structure-lesson.dto';
4
5
  import { CreateCourseStructureSessionDto } from './dto/create-course-structure-session.dto';
6
+ import { MoveLessonDto } from './dto/move-lesson.dto';
7
+ import { PasteLessonsDto } from './dto/paste-lessons.dto';
8
+ import { ReorderLessonsDto } from './dto/reorder-lessons.dto';
9
+ import { ReorderSessionsDto } from './dto/reorder-sessions.dto';
5
10
  import { UpdateCourseStructureLessonDto } from './dto/update-course-structure-lesson.dto';
6
11
  import { UpdateCourseStructureSessionDto } from './dto/update-course-structure-session.dto';
7
- import { InstructorService } from '../instructor/instructor.service';
8
12
 
9
13
  type LessonUiType = 'video' | 'questao' | 'post' | 'exercicio';
10
14
  type LessonDbType = 'video' | 'text' | 'quiz';
@@ -19,7 +23,7 @@ export class CourseStructureService {
19
23
  async getStructure(courseId: number) {
20
24
  await this.ensureCourseExists(courseId);
21
25
 
22
- const [modules, instructors] = await Promise.all([
26
+ const [modules, instructors, courseRecord] = await Promise.all([
23
27
  this.prisma.course_module.findMany({
24
28
  where: { course_id: courseId },
25
29
  orderBy: { order: 'asc' },
@@ -50,6 +54,10 @@ export class CourseStructureService {
50
54
  },
51
55
  }),
52
56
  this.instructorService.listQualifiedInstructorOptions(['course-lessons']),
57
+ this.prisma.course.findUnique({
58
+ where: { id: courseId },
59
+ select: { id: true, slug: true, title: true, description: true },
60
+ }),
53
61
  ]);
54
62
 
55
63
  const sessoes = modules.map((module, index) => ({
@@ -108,6 +116,14 @@ export class CourseStructureService {
108
116
  id: instructor.id,
109
117
  name: instructor.name,
110
118
  })),
119
+ curso: courseRecord
120
+ ? {
121
+ id: String(courseRecord.id),
122
+ slug: courseRecord.slug ?? '',
123
+ titulo: courseRecord.title,
124
+ descricao: courseRecord.description ?? '',
125
+ }
126
+ : null,
111
127
  };
112
128
  }
113
129
 
@@ -281,8 +297,379 @@ export class CourseStructureService {
281
297
  return { success: true };
282
298
  }
283
299
 
284
- private async syncLessonRelations(
300
+ async reorderSessions(courseId: number, dto: ReorderSessionsDto) {
301
+ await this.ensureCourseExists(courseId);
302
+
303
+ // Validate that all provided IDs belong to the course
304
+ const existing = await this.prisma.course_module.findMany({
305
+ where: { course_id: courseId },
306
+ select: { id: true },
307
+ });
308
+ const existingIds = new Set(existing.map((m) => m.id));
309
+ for (const id of dto.sessionIds) {
310
+ if (!existingIds.has(id)) {
311
+ throw new BadRequestException(`Session ${id} does not belong to course ${courseId}`);
312
+ }
313
+ }
314
+
315
+ await this.prisma.$transaction(
316
+ dto.sessionIds.map((id, index) =>
317
+ this.prisma.course_module.update({
318
+ where: { id },
319
+ data: { order: index + 1 },
320
+ }),
321
+ ),
322
+ );
323
+
324
+ return { success: true };
325
+ }
326
+
327
+ async reorderLessons(courseId: number, sessionId: number, dto: ReorderLessonsDto) {
328
+ const module = await this.prisma.course_module.findFirst({
329
+ where: { id: sessionId, course_id: courseId },
330
+ });
331
+
332
+ if (!module) {
333
+ throw new NotFoundException('Session not found for this course');
334
+ }
335
+
336
+ const existing = await this.prisma.course_lesson.findMany({
337
+ where: { course_module_id: sessionId },
338
+ select: { id: true },
339
+ });
340
+ const existingIds = new Set(existing.map((l) => l.id));
341
+ for (const id of dto.lessonIds) {
342
+ if (!existingIds.has(id)) {
343
+ throw new BadRequestException(`Lesson ${id} does not belong to session ${sessionId}`);
344
+ }
345
+ }
346
+
347
+ await this.prisma.$transaction(
348
+ dto.lessonIds.map((id, index) =>
349
+ this.prisma.course_lesson.update({
350
+ where: { id },
351
+ data: { order: index + 1 },
352
+ }),
353
+ ),
354
+ );
355
+
356
+ return { success: true };
357
+ }
358
+
359
+ async moveLesson(
360
+ courseId: number,
361
+ sessionId: number,
285
362
  lessonId: number,
363
+ dto: MoveLessonDto,
364
+ ) {
365
+ // Ensure the lesson belongs to the source session
366
+ const lesson = await this.prisma.course_lesson.findFirst({
367
+ where: {
368
+ id: lessonId,
369
+ course_module_id: sessionId,
370
+ course_module: { course_id: courseId },
371
+ },
372
+ });
373
+
374
+ if (!lesson) {
375
+ throw new NotFoundException('Lesson not found for this session');
376
+ }
377
+
378
+ // Ensure the target session belongs to the same course
379
+ const targetModule = await this.prisma.course_module.findFirst({
380
+ where: { id: dto.toSessionId, course_id: courseId },
381
+ });
382
+
383
+ if (!targetModule) {
384
+ throw new NotFoundException('Target session not found for this course');
385
+ }
386
+
387
+ if (dto.toSessionId === sessionId) {
388
+ // Same session — just reorder
389
+ const siblings = await this.prisma.course_lesson.findMany({
390
+ where: { course_module_id: sessionId, id: { not: lessonId } },
391
+ orderBy: { order: 'asc' },
392
+ select: { id: true },
393
+ });
394
+ const insertAt = Math.min(dto.toIndex ?? siblings.length, siblings.length);
395
+ const orderedIds = [
396
+ ...siblings.slice(0, insertAt).map((l) => l.id),
397
+ lessonId,
398
+ ...siblings.slice(insertAt).map((l) => l.id),
399
+ ];
400
+ await this.prisma.$transaction(
401
+ orderedIds.map((id, index) =>
402
+ this.prisma.course_lesson.update({
403
+ where: { id },
404
+ data: { order: index + 1 },
405
+ }),
406
+ ),
407
+ );
408
+ } else {
409
+ // Move to a different session
410
+ const siblings = await this.prisma.course_lesson.findMany({
411
+ where: { course_module_id: dto.toSessionId },
412
+ orderBy: { order: 'asc' },
413
+ select: { id: true },
414
+ });
415
+ const insertAt = Math.min(dto.toIndex ?? siblings.length, siblings.length);
416
+ const orderedIds = [
417
+ ...siblings.slice(0, insertAt).map((l) => l.id),
418
+ lessonId,
419
+ ...siblings.slice(insertAt).map((l) => l.id),
420
+ ];
421
+
422
+ await this.prisma.$transaction([
423
+ this.prisma.course_lesson.update({
424
+ where: { id: lessonId },
425
+ data: { course_module_id: dto.toSessionId },
426
+ }),
427
+ ...orderedIds.map((id, index) =>
428
+ this.prisma.course_lesson.update({
429
+ where: { id },
430
+ data: { order: index + 1 },
431
+ }),
432
+ ),
433
+ ]);
434
+ }
435
+
436
+ return { success: true };
437
+ }
438
+
439
+ async duplicateSession(courseId: number, sessionId: number) {
440
+ const sourceModule = await this.prisma.course_module.findFirst({
441
+ where: { id: sessionId, course_id: courseId },
442
+ include: {
443
+ course_lesson: {
444
+ orderBy: { order: 'asc' },
445
+ include: {
446
+ course_lesson_file: true,
447
+ course_lesson_question: true,
448
+ course_lesson_instructor: true,
449
+ },
450
+ },
451
+ },
452
+ });
453
+
454
+ if (!sourceModule) {
455
+ throw new NotFoundException('Session not found for this course');
456
+ }
457
+
458
+ const nextOrder = await this.nextModuleOrder(courseId);
459
+
460
+ const newModule = await this.prisma.course_module.create({
461
+ data: {
462
+ course_id: courseId,
463
+ title: `Cópia de ${sourceModule.title}`,
464
+ description: sourceModule.description,
465
+ duration_minutes: sourceModule.duration_minutes,
466
+ order: nextOrder,
467
+ },
468
+ });
469
+
470
+ const newLessons = [];
471
+ for (const lesson of sourceModule.course_lesson) {
472
+ const lessonOrder = await this.nextLessonOrder(newModule.id);
473
+ const newLesson = await this.prisma.course_lesson.create({
474
+ data: {
475
+ course_module_id: newModule.id,
476
+ title: lesson.title,
477
+ type: lesson.type,
478
+ description: lesson.description,
479
+ duration_seconds: lesson.duration_seconds,
480
+ content: lesson.content,
481
+ order: lessonOrder,
482
+ },
483
+ });
484
+
485
+ if (lesson.course_lesson_file.length > 0) {
486
+ await this.prisma.course_lesson_file.createMany({
487
+ data: lesson.course_lesson_file.map((f) => ({
488
+ course_lesson_id: newLesson.id,
489
+ title: f.title,
490
+ file_id: f.file_id,
491
+ })),
492
+ });
493
+ }
494
+
495
+ if (lesson.course_lesson_instructor.length > 0) {
496
+ await this.prisma.course_lesson_instructor.createMany({
497
+ data: lesson.course_lesson_instructor.map((i) => ({
498
+ course_lesson_id: newLesson.id,
499
+ instructor_id: i.instructor_id,
500
+ role: i.role,
501
+ })),
502
+ });
503
+ }
504
+
505
+ if (lesson.course_lesson_question.length > 0) {
506
+ await this.prisma.course_lesson_question.createMany({
507
+ data: lesson.course_lesson_question.map((q) => ({
508
+ course_lesson_id: newLesson.id,
509
+ question_id: q.question_id,
510
+ order: q.order,
511
+ })),
512
+ });
513
+ }
514
+
515
+ newLessons.push(await this.getLessonById(newLesson.id));
516
+ }
517
+
518
+ return {
519
+ session: {
520
+ id: String(newModule.id),
521
+ codigo: this.formatCode('S', newModule.order ?? nextOrder),
522
+ titulo: newModule.title,
523
+ duracao: newModule.duration_minutes ?? 0,
524
+ collapsed: false,
525
+ },
526
+ lessons: newLessons,
527
+ };
528
+ }
529
+
530
+ async duplicateLesson(courseId: number, sessionId: number, lessonId: number) {
531
+ const sourceLesson = await this.prisma.course_lesson.findFirst({
532
+ where: {
533
+ id: lessonId,
534
+ course_module_id: sessionId,
535
+ course_module: { course_id: courseId },
536
+ },
537
+ include: {
538
+ course_lesson_file: true,
539
+ course_lesson_question: true,
540
+ course_lesson_instructor: true,
541
+ },
542
+ });
543
+
544
+ if (!sourceLesson) {
545
+ throw new NotFoundException('Lesson not found for this session');
546
+ }
547
+
548
+ const nextOrder = await this.nextLessonOrder(sessionId);
549
+
550
+ const newLesson = await this.prisma.course_lesson.create({
551
+ data: {
552
+ course_module_id: sessionId,
553
+ title: `Cópia de ${sourceLesson.title}`,
554
+ type: sourceLesson.type,
555
+ description: sourceLesson.description,
556
+ duration_seconds: sourceLesson.duration_seconds,
557
+ content: sourceLesson.content,
558
+ order: nextOrder,
559
+ },
560
+ });
561
+
562
+ if (sourceLesson.course_lesson_file.length > 0) {
563
+ await this.prisma.course_lesson_file.createMany({
564
+ data: sourceLesson.course_lesson_file.map((f) => ({
565
+ course_lesson_id: newLesson.id,
566
+ title: f.title,
567
+ file_id: f.file_id,
568
+ })),
569
+ });
570
+ }
571
+
572
+ if (sourceLesson.course_lesson_instructor.length > 0) {
573
+ await this.prisma.course_lesson_instructor.createMany({
574
+ data: sourceLesson.course_lesson_instructor.map((i) => ({
575
+ course_lesson_id: newLesson.id,
576
+ instructor_id: i.instructor_id,
577
+ role: i.role,
578
+ })),
579
+ });
580
+ }
581
+
582
+ if (sourceLesson.course_lesson_question.length > 0) {
583
+ await this.prisma.course_lesson_question.createMany({
584
+ data: sourceLesson.course_lesson_question.map((q) => ({
585
+ course_lesson_id: newLesson.id,
586
+ question_id: q.question_id,
587
+ order: q.order,
588
+ })),
589
+ });
590
+ }
591
+
592
+ return this.getLessonById(newLesson.id);
593
+ }
594
+
595
+ async pasteLessons(courseId: number, targetSessionId: number, dto: PasteLessonsDto) {
596
+ const targetModule = await this.prisma.course_module.findFirst({
597
+ where: { id: targetSessionId, course_id: courseId },
598
+ });
599
+
600
+ if (!targetModule) {
601
+ throw new NotFoundException('Target session not found for this course');
602
+ }
603
+
604
+ const sourceLessons = await this.prisma.course_lesson.findMany({
605
+ where: {
606
+ id: { in: dto.lessonIds },
607
+ course_module: { course_id: courseId },
608
+ },
609
+ orderBy: { order: 'asc' },
610
+ include: {
611
+ course_lesson_file: true,
612
+ course_lesson_question: true,
613
+ course_lesson_instructor: true,
614
+ },
615
+ });
616
+
617
+ if (sourceLessons.length !== dto.lessonIds.length) {
618
+ throw new BadRequestException('Some lessons do not belong to this course');
619
+ }
620
+
621
+ const result = [];
622
+ for (const lesson of sourceLessons) {
623
+ const lessonOrder = await this.nextLessonOrder(targetSessionId);
624
+ const newLesson = await this.prisma.course_lesson.create({
625
+ data: {
626
+ course_module_id: targetSessionId,
627
+ title: `Cópia de ${lesson.title}`,
628
+ type: lesson.type,
629
+ description: lesson.description,
630
+ duration_seconds: lesson.duration_seconds,
631
+ content: lesson.content,
632
+ order: lessonOrder,
633
+ },
634
+ });
635
+
636
+ if (lesson.course_lesson_file.length > 0) {
637
+ await this.prisma.course_lesson_file.createMany({
638
+ data: lesson.course_lesson_file.map((f) => ({
639
+ course_lesson_id: newLesson.id,
640
+ title: f.title,
641
+ file_id: f.file_id,
642
+ })),
643
+ });
644
+ }
645
+
646
+ if (lesson.course_lesson_instructor.length > 0) {
647
+ await this.prisma.course_lesson_instructor.createMany({
648
+ data: lesson.course_lesson_instructor.map((i) => ({
649
+ course_lesson_id: newLesson.id,
650
+ instructor_id: i.instructor_id,
651
+ role: i.role,
652
+ })),
653
+ });
654
+ }
655
+
656
+ if (lesson.course_lesson_question.length > 0) {
657
+ await this.prisma.course_lesson_question.createMany({
658
+ data: lesson.course_lesson_question.map((q) => ({
659
+ course_lesson_id: newLesson.id,
660
+ question_id: q.question_id,
661
+ order: q.order,
662
+ })),
663
+ });
664
+ }
665
+
666
+ result.push(await this.getLessonById(newLesson.id));
667
+ }
668
+
669
+ return { lessons: result };
670
+ }
671
+
672
+ private async syncLessonRelations( lessonId: number,
286
673
  dto: Partial<
287
674
  Pick<
288
675
  CreateCourseStructureLessonDto,