@hed-hog/lms 0.0.357 → 0.0.358

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 (57) hide show
  1. package/dist/course/course-operations-integration.service.d.ts +31 -0
  2. package/dist/course/course-operations-integration.service.d.ts.map +1 -1
  3. package/dist/course/course-operations-integration.service.js +286 -22
  4. package/dist/course/course-operations-integration.service.js.map +1 -1
  5. package/dist/course/course-operations.controller.d.ts +10 -0
  6. package/dist/course/course-operations.controller.d.ts.map +1 -0
  7. package/dist/course/course-operations.controller.js +67 -0
  8. package/dist/course/course-operations.controller.js.map +1 -0
  9. package/dist/course/course-structure.controller.d.ts +3 -1
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.service.d.ts +3 -1
  12. package/dist/course/course-structure.service.d.ts.map +1 -1
  13. package/dist/course/course-structure.service.js +13 -6
  14. package/dist/course/course-structure.service.js.map +1 -1
  15. package/dist/course/course.module.d.ts.map +1 -1
  16. package/dist/course/course.module.js +4 -1
  17. package/dist/course/course.module.js.map +1 -1
  18. package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
  19. package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
  20. package/dist/course/dto/update-course-operations-config.dto.js +33 -0
  21. package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
  22. package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
  23. package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
  24. package/dist/course/lms-operations-task.subscriber.js +57 -0
  25. package/dist/course/lms-operations-task.subscriber.js.map +1 -0
  26. package/dist/enterprise/enterprise.service.js +1 -1
  27. package/dist/enterprise/enterprise.service.js.map +1 -1
  28. package/dist/instructor/instructor.service.d.ts.map +1 -1
  29. package/dist/instructor/instructor.service.js +12 -3
  30. package/dist/instructor/instructor.service.js.map +1 -1
  31. package/hedhog/data/route.yaml +27 -0
  32. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
  33. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
  34. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
  35. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
  36. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
  37. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +31 -1
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +91 -20
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -0
  42. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +11 -3
  43. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -2
  44. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
  45. package/hedhog/frontend/app/courses/page.tsx.ejs +21 -88
  46. package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
  47. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
  48. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
  49. package/package.json +8 -8
  50. package/src/course/course-operations-integration.service.ts +460 -22
  51. package/src/course/course-operations.controller.ts +45 -0
  52. package/src/course/course-structure.service.ts +5 -1
  53. package/src/course/course.module.ts +4 -1
  54. package/src/course/dto/update-course-operations-config.dto.ts +16 -0
  55. package/src/course/lms-operations-task.subscriber.ts +44 -0
  56. package/src/enterprise/enterprise.service.ts +1 -1
  57. package/src/instructor/instructor.service.ts +12 -3
@@ -16,6 +16,32 @@ type LessonTaskLink = {
16
16
  projectId: number | null;
17
17
  };
18
18
 
19
+ export type OperationsConfig = {
20
+ isAvailable: boolean;
21
+ projectId: number | null;
22
+ projectCode: string | null;
23
+ projectName: string | null;
24
+ taskMode: 'per_lesson' | 'per_production_status' | null;
25
+ completionStatus: string | null;
26
+ missingTasksCount: number;
27
+ };
28
+
29
+ export type UpdateOperationsConfigInput = {
30
+ projectId?: number | null;
31
+ taskMode?: 'per_lesson' | 'per_production_status' | null;
32
+ completionStatus?: string | null;
33
+ };
34
+
35
+ const PRODUCTION_STAGES = [
36
+ 'preparacao',
37
+ 'gravacao',
38
+ 'edicao',
39
+ 'finalizacao',
40
+ 'publicacao',
41
+ ] as const;
42
+
43
+ type ProductionStage = (typeof PRODUCTION_STAGES)[number];
44
+
19
45
  @Injectable()
20
46
  export class CourseOperationsIntegrationService {
21
47
  private operationsAvailability: boolean | null = null;
@@ -51,6 +77,162 @@ export class CourseOperationsIntegrationService {
51
77
  return this.operationsAvailability;
52
78
  }
53
79
 
80
+ async getOperationsConfig(
81
+ courseId: number,
82
+ client: PrismaClientLike = this.prisma,
83
+ ): Promise<OperationsConfig> {
84
+ const isAvailable = await this.isOperationsModuleAvailable(client);
85
+
86
+ const courseRows = (await client.$queryRawUnsafe(
87
+ `
88
+ SELECT c.operations_project_id AS "projectId",
89
+ c.operations_task_mode AS "taskMode",
90
+ c.operations_lesson_completion_status AS "completionStatus"
91
+ FROM course c
92
+ WHERE c.id = $1
93
+ LIMIT 1
94
+ `,
95
+ courseId,
96
+ )) as Array<{
97
+ projectId: number | null;
98
+ taskMode: string | null;
99
+ completionStatus: string | null;
100
+ }>;
101
+
102
+ const courseRow = courseRows[0];
103
+ if (!courseRow) {
104
+ return {
105
+ isAvailable,
106
+ projectId: null,
107
+ projectCode: null,
108
+ projectName: null,
109
+ taskMode: null,
110
+ completionStatus: null,
111
+ missingTasksCount: 0,
112
+ };
113
+ }
114
+
115
+ const projectId = courseRow.projectId;
116
+ const taskMode = (courseRow.taskMode ?? null) as
117
+ | 'per_lesson'
118
+ | 'per_production_status'
119
+ | null;
120
+ const completionStatus = courseRow.completionStatus ?? null;
121
+
122
+ let projectCode: string | null = null;
123
+ let projectName: string | null = null;
124
+
125
+ if (projectId && isAvailable) {
126
+ const projectRows = (await client.$queryRawUnsafe(
127
+ `
128
+ SELECT code AS "projectCode", name AS "projectName"
129
+ FROM operations_project
130
+ WHERE id = $1 AND deleted_at IS NULL
131
+ LIMIT 1
132
+ `,
133
+ projectId,
134
+ )) as Array<{ projectCode: string | null; projectName: string | null }>;
135
+
136
+ projectCode = projectRows[0]?.projectCode ?? null;
137
+ projectName = projectRows[0]?.projectName ?? null;
138
+ }
139
+
140
+ const missingTasksCount = await this.countMissingTasks(
141
+ courseId,
142
+ taskMode,
143
+ client,
144
+ );
145
+
146
+ return {
147
+ isAvailable,
148
+ projectId,
149
+ projectCode,
150
+ projectName,
151
+ taskMode,
152
+ completionStatus,
153
+ missingTasksCount,
154
+ };
155
+ }
156
+
157
+ private async countMissingTasks(
158
+ courseId: number,
159
+ taskMode: 'per_lesson' | 'per_production_status' | null,
160
+ client: PrismaClientLike,
161
+ ): Promise<number> {
162
+ if (!taskMode) return 0;
163
+
164
+ try {
165
+ if (taskMode === 'per_lesson') {
166
+ const rows = (await client.$queryRawUnsafe(
167
+ `
168
+ SELECT COUNT(*) AS cnt
169
+ FROM course_lesson l
170
+ JOIN course_module m ON m.id = l.course_module_id
171
+ WHERE m.course_id = $1
172
+ AND l.operations_task_id IS NULL
173
+ `,
174
+ courseId,
175
+ )) as Array<{ cnt: string | number }>;
176
+ return Number(rows[0]?.cnt ?? 0);
177
+ }
178
+
179
+ const rows = (await client.$queryRawUnsafe(
180
+ `
181
+ SELECT COUNT(DISTINCT l.id) AS cnt
182
+ FROM course_lesson l
183
+ JOIN course_module m ON m.id = l.course_module_id
184
+ WHERE m.course_id = $1
185
+ AND (
186
+ SELECT COUNT(*)
187
+ FROM course_lesson_operations_task clot
188
+ WHERE clot.course_lesson_id = l.id
189
+ ) < $2
190
+ `,
191
+ courseId,
192
+ PRODUCTION_STAGES.length,
193
+ )) as Array<{ cnt: string | number }>;
194
+ return Number(rows[0]?.cnt ?? 0);
195
+ } catch {
196
+ return 0;
197
+ }
198
+ }
199
+
200
+ async updateOperationsConfig(
201
+ courseId: number,
202
+ input: UpdateOperationsConfigInput,
203
+ client: PrismaClientLike = this.prisma,
204
+ ) {
205
+ const setParts: string[] = [];
206
+ const values: unknown[] = [];
207
+ let idx = 1;
208
+
209
+ if (input.projectId !== undefined) {
210
+ if (input.projectId !== null && input.projectId > 0) {
211
+ await this.resolveOperationsProjectId(input.projectId, client);
212
+ }
213
+ setParts.push(`operations_project_id = $${idx++}`);
214
+ values.push(input.projectId ?? null);
215
+ }
216
+
217
+ if (input.taskMode !== undefined) {
218
+ setParts.push(`operations_task_mode = $${idx++}`);
219
+ values.push(input.taskMode ?? null);
220
+ }
221
+
222
+ if (input.completionStatus !== undefined) {
223
+ setParts.push(`operations_lesson_completion_status = $${idx++}`);
224
+ values.push(input.completionStatus ?? null);
225
+ }
226
+
227
+ if (setParts.length === 0) return;
228
+
229
+ values.push(courseId);
230
+ await client.$executeRawUnsafe(
231
+ `UPDATE course SET ${setParts.join(', ')}, updated_at = NOW() WHERE id = $${idx}`,
232
+ ...values,
233
+ );
234
+ }
235
+
54
236
  async resolveOperationsProjectId(
55
237
  value?: number | null,
56
238
  client: PrismaClientLike = this.prisma,
@@ -373,6 +555,150 @@ export class CourseOperationsIntegrationService {
373
555
  return taskId;
374
556
  }
375
557
 
558
+ async createProductionTasksForLesson(
559
+ client: PrismaClientLike,
560
+ input: {
561
+ courseId: number;
562
+ lessonId: number;
563
+ projectId: number;
564
+ courseTitle?: string | null;
565
+ sessionTitle?: string | null;
566
+ lessonTitle: string;
567
+ lessonDescription?: string | null;
568
+ },
569
+ ) {
570
+ if (!(await this.isOperationsModuleAvailable(client))) {
571
+ return 0;
572
+ }
573
+
574
+ const projectRows = (await client.$queryRawUnsafe(
575
+ `SELECT id FROM operations_project WHERE id = $1 AND deleted_at IS NULL LIMIT 1`,
576
+ input.projectId,
577
+ )) as Array<{ id: number }>;
578
+
579
+ if (!projectRows[0]?.id) {
580
+ throw new BadRequestException('Operations project not found');
581
+ }
582
+
583
+ const maxPositionRows = (await client.$queryRawUnsafe(
584
+ `
585
+ SELECT MAX(position) AS max_position
586
+ FROM operations_task
587
+ WHERE project_id = $1 AND status = 'todo' AND deleted_at IS NULL
588
+ `,
589
+ input.projectId,
590
+ )) as Array<{ max_position: number | null }>;
591
+
592
+ let nextPosition = Number(maxPositionRows[0]?.max_position ?? -1) + 1;
593
+ let created = 0;
594
+
595
+ for (const stage of PRODUCTION_STAGES) {
596
+ const existing = (await client.$queryRawUnsafe(
597
+ `SELECT id FROM course_lesson_operations_task WHERE course_lesson_id = $1 AND production_stage = $2 LIMIT 1`,
598
+ input.lessonId,
599
+ stage,
600
+ )) as Array<{ id: number }>;
601
+
602
+ if (existing[0]?.id) continue;
603
+
604
+ const stageLabel = this.stageLabel(stage);
605
+ const taskName = `${this.buildTaskName(input.lessonTitle)} — ${stageLabel}`;
606
+ const taskDescription = this.buildTaskDescription({
607
+ courseTitle: input.courseTitle,
608
+ sessionTitle: input.sessionTitle,
609
+ lessonDescription: input.lessonDescription,
610
+ });
611
+ const tags = `lms,course-${input.courseId},lesson-${input.lessonId},stage-${stage}`;
612
+
613
+ const createdRows = (await client.$queryRawUnsafe(
614
+ `
615
+ INSERT INTO operations_task (
616
+ project_id, name, description, priority, status, position,
617
+ tags, total_doing_minutes, created_at, updated_at
618
+ )
619
+ VALUES ($1, $2, $3, 'medium', 'todo', $4, $5, 0, NOW(), NOW())
620
+ RETURNING id
621
+ `,
622
+ input.projectId,
623
+ taskName,
624
+ taskDescription,
625
+ nextPosition++,
626
+ tags,
627
+ )) as Array<{ id: number }>;
628
+
629
+ const taskId = createdRows[0]?.id;
630
+ if (!taskId) continue;
631
+
632
+ await client.$executeRawUnsafe(
633
+ `INSERT INTO operations_task_activity (task_id, actor_collaborator_id, action, created_at)
634
+ VALUES ($1, NULL, 'task_created', NOW())`,
635
+ taskId,
636
+ );
637
+
638
+ await client.$executeRawUnsafe(
639
+ `INSERT INTO course_lesson_operations_task (course_lesson_id, operations_task_id, production_stage, created_at, updated_at)
640
+ VALUES ($1, $2, $3, NOW(), NOW())
641
+ ON CONFLICT (course_lesson_id, production_stage) DO NOTHING`,
642
+ input.lessonId,
643
+ taskId,
644
+ stage,
645
+ );
646
+
647
+ created++;
648
+ }
649
+
650
+ return created;
651
+ }
652
+
653
+ async deleteTasksForLesson(
654
+ lessonId: number,
655
+ client: PrismaClientLike = this.prisma,
656
+ ) {
657
+ if (!(await this.isOperationsModuleAvailable(client))) {
658
+ return;
659
+ }
660
+
661
+ try {
662
+ // per_lesson mode: delete the single linked task
663
+ const lessonRows = (await client.$queryRawUnsafe(
664
+ `SELECT operations_task_id AS "taskId" FROM course_lesson WHERE id = $1 LIMIT 1`,
665
+ lessonId,
666
+ )) as Array<{ taskId: number | null }>;
667
+
668
+ const singleTaskId = lessonRows[0]?.taskId ?? null;
669
+ if (singleTaskId) {
670
+ await client.$executeRawUnsafe(
671
+ `UPDATE operations_task SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1 AND deleted_at IS NULL`,
672
+ singleTaskId,
673
+ );
674
+ await client.$executeRawUnsafe(
675
+ `UPDATE course_lesson SET operations_task_id = NULL WHERE id = $1`,
676
+ lessonId,
677
+ );
678
+ }
679
+
680
+ // per_production_status mode: soft-delete all linked production tasks
681
+ const multiRows = (await client.$queryRawUnsafe(
682
+ `SELECT operations_task_id AS "taskId" FROM course_lesson_operations_task WHERE course_lesson_id = $1`,
683
+ lessonId,
684
+ )) as Array<{ taskId: number }>;
685
+
686
+ for (const row of multiRows) {
687
+ await client.$executeRawUnsafe(
688
+ `UPDATE operations_task SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1 AND deleted_at IS NULL`,
689
+ row.taskId,
690
+ );
691
+ }
692
+
693
+ await client.$executeRawUnsafe(
694
+ `DELETE FROM course_lesson_operations_task WHERE course_lesson_id = $1`,
695
+ lessonId,
696
+ );
697
+ } catch {
698
+ // Graceful no-op if tables not present yet
699
+ }
700
+ }
701
+
376
702
  async syncTaskForLesson(
377
703
  lessonId: number,
378
704
  client: PrismaClientLike = this.prisma,
@@ -433,14 +759,21 @@ export class CourseOperationsIntegrationService {
433
759
  courseId: number,
434
760
  client: PrismaClientLike = this.prisma,
435
761
  ) {
436
- const projectLinks = await this.getCourseProjectLinks([courseId], client);
437
- const projectId = projectLinks.get(courseId)?.projectId ?? null;
762
+ const courseRows = (await client.$queryRawUnsafe(
763
+ `SELECT operations_project_id AS "projectId", operations_task_mode AS "taskMode"
764
+ FROM course WHERE id = $1 LIMIT 1`,
765
+ courseId,
766
+ )) as Array<{ projectId: number | null; taskMode: string | null }>;
438
767
 
439
- if (!projectId || !(await this.isOperationsModuleAvailable(client))) {
768
+ const courseRow = courseRows[0];
769
+ const projectId = courseRow?.projectId ?? null;
770
+ const taskMode = courseRow?.taskMode ?? null;
771
+
772
+ if (!projectId || !taskMode || !(await this.isOperationsModuleAvailable(client))) {
440
773
  return 0;
441
774
  }
442
775
 
443
- const rows = (await client.$queryRawUnsafe(
776
+ const lessonsRows = (await client.$queryRawUnsafe(
444
777
  `
445
778
  SELECT l.id,
446
779
  l.title AS "lessonTitle",
@@ -448,12 +781,9 @@ export class CourseOperationsIntegrationService {
448
781
  m.title AS "sessionTitle",
449
782
  c.title AS "courseTitle"
450
783
  FROM course_lesson l
451
- JOIN course_module m
452
- ON m.id = l.course_module_id
453
- JOIN course c
454
- ON c.id = m.course_id
784
+ JOIN course_module m ON m.id = l.course_module_id
785
+ JOIN course c ON c.id = m.course_id
455
786
  WHERE c.id = $1
456
- AND l.operations_task_id IS NULL
457
787
  ORDER BY m."order" ASC, l."order" ASC, l.id ASC
458
788
  `,
459
789
  courseId,
@@ -467,25 +797,133 @@ export class CourseOperationsIntegrationService {
467
797
 
468
798
  let createdCount = 0;
469
799
 
470
- for (const row of rows) {
471
- const taskId = await this.createTaskForLesson(client, {
472
- courseId,
473
- lessonId: row.id,
474
- projectId,
475
- courseTitle: row.courseTitle,
476
- sessionTitle: row.sessionTitle,
477
- lessonTitle: row.lessonTitle,
478
- lessonDescription: row.lessonDescription,
479
- });
480
-
481
- if (taskId) {
482
- createdCount += 1;
800
+ for (const row of lessonsRows) {
801
+ if (taskMode === 'per_lesson') {
802
+ const existing = (await client.$queryRawUnsafe(
803
+ `SELECT operations_task_id FROM course_lesson WHERE id = $1 LIMIT 1`,
804
+ row.id,
805
+ )) as Array<{ operations_task_id: number | null }>;
806
+ if (existing[0]?.operations_task_id) continue;
807
+
808
+ const taskId = await this.createTaskForLesson(client, {
809
+ courseId,
810
+ lessonId: row.id,
811
+ projectId,
812
+ courseTitle: row.courseTitle,
813
+ sessionTitle: row.sessionTitle,
814
+ lessonTitle: row.lessonTitle,
815
+ lessonDescription: row.lessonDescription,
816
+ });
817
+ if (taskId) createdCount++;
818
+ } else if (taskMode === 'per_production_status') {
819
+ const n = await this.createProductionTasksForLesson(client, {
820
+ courseId,
821
+ lessonId: row.id,
822
+ projectId,
823
+ courseTitle: row.courseTitle,
824
+ sessionTitle: row.sessionTitle,
825
+ lessonTitle: row.lessonTitle,
826
+ lessonDescription: row.lessonDescription,
827
+ });
828
+ createdCount += n;
483
829
  }
484
830
  }
485
831
 
486
832
  return createdCount;
487
833
  }
488
834
 
835
+ async applyTaskCompletionToLesson(
836
+ taskId: number,
837
+ taskTags: string,
838
+ client: PrismaClientLike = this.prisma,
839
+ ) {
840
+ if (!(await this.isOperationsModuleAvailable(client))) return;
841
+
842
+ // Detect per_lesson mode: lesson is linked via course_lesson.operations_task_id
843
+ const perLessonRows = (await client.$queryRawUnsafe(
844
+ `
845
+ SELECT l.id AS "lessonId", c.operations_lesson_completion_status AS "completionStatus"
846
+ FROM course_lesson l
847
+ JOIN course_module m ON m.id = l.course_module_id
848
+ JOIN course c ON c.id = m.course_id
849
+ WHERE l.operations_task_id = $1
850
+ AND c.operations_task_mode = 'per_lesson'
851
+ AND c.operations_lesson_completion_status IS NOT NULL
852
+ LIMIT 1
853
+ `,
854
+ taskId,
855
+ )) as Array<{ lessonId: number; completionStatus: string }>;
856
+
857
+ if (perLessonRows[0]) {
858
+ const { lessonId, completionStatus } = perLessonRows[0];
859
+ await this.applyLessonProductionStatus(lessonId, completionStatus, client);
860
+ return;
861
+ }
862
+
863
+ // Detect per_production_status mode: lesson is linked via course_lesson_operations_task
864
+ const perStageRows = (await client.$queryRawUnsafe(
865
+ `
866
+ SELECT clot.course_lesson_id AS "lessonId", clot.production_stage AS "stage"
867
+ FROM course_lesson_operations_task clot
868
+ WHERE clot.operations_task_id = $1
869
+ LIMIT 1
870
+ `,
871
+ taskId,
872
+ )) as Array<{ lessonId: number; stage: string }>;
873
+
874
+ if (perStageRows[0]) {
875
+ const { lessonId, stage } = perStageRows[0];
876
+ const statusForStage = this.stageToProductionStatus(
877
+ stage as ProductionStage,
878
+ taskTags,
879
+ );
880
+ if (statusForStage) {
881
+ await this.applyLessonProductionStatus(lessonId, statusForStage, client);
882
+ }
883
+ }
884
+ }
885
+
886
+ private async applyLessonProductionStatus(
887
+ lessonId: number,
888
+ status: string,
889
+ client: PrismaClientLike,
890
+ ) {
891
+ try {
892
+ await client.$executeRawUnsafe(
893
+ `UPDATE course_lesson SET status_producao = $1, updated_at = NOW() WHERE id = $2`,
894
+ status,
895
+ lessonId,
896
+ );
897
+ } catch {
898
+ // Column may not exist in older schema versions — ignore gracefully
899
+ }
900
+ }
901
+
902
+ private stageToProductionStatus(
903
+ stage: ProductionStage,
904
+ _tags: string,
905
+ ): string | null {
906
+ const map: Record<ProductionStage, string> = {
907
+ preparacao: 'preparada',
908
+ gravacao: 'gravada',
909
+ edicao: 'editada',
910
+ finalizacao: 'finalizada',
911
+ publicacao: 'publicada',
912
+ };
913
+ return map[stage] ?? null;
914
+ }
915
+
916
+ private stageLabel(stage: ProductionStage): string {
917
+ const labels: Record<ProductionStage, string> = {
918
+ preparacao: 'Preparação',
919
+ gravacao: 'Gravação',
920
+ edicao: 'Edição',
921
+ finalizacao: 'Finalização',
922
+ publicacao: 'Publicação',
923
+ };
924
+ return labels[stage];
925
+ }
926
+
489
927
  private normalizeIds(ids: number[]) {
490
928
  return [...new Set(ids)]
491
929
  .map((id) => Number(id))
@@ -0,0 +1,45 @@
1
+ import { Role } from '@hed-hog/api';
2
+ import {
3
+ Body,
4
+ Controller,
5
+ Get,
6
+ HttpCode,
7
+ HttpStatus,
8
+ Param,
9
+ ParseIntPipe,
10
+ Patch,
11
+ Post,
12
+ } from '@nestjs/common';
13
+ import { CourseOperationsIntegrationService } from './course-operations-integration.service';
14
+ import { UpdateCourseOperationsConfigDto } from './dto/update-course-operations-config.dto';
15
+
16
+ @Role()
17
+ @Controller('lms/courses/:id/operations')
18
+ export class CourseOperationsController {
19
+ constructor(
20
+ private readonly operationsIntegration: CourseOperationsIntegrationService,
21
+ ) {}
22
+
23
+ @Get()
24
+ getConfig(@Param('id', ParseIntPipe) courseId: number) {
25
+ return this.operationsIntegration.getOperationsConfig(courseId);
26
+ }
27
+
28
+ @Patch()
29
+ updateConfig(
30
+ @Param('id', ParseIntPipe) courseId: number,
31
+ @Body() dto: UpdateCourseOperationsConfigDto,
32
+ ) {
33
+ return this.operationsIntegration.updateOperationsConfig(courseId, {
34
+ projectId: dto.projectId,
35
+ taskMode: dto.taskMode,
36
+ completionStatus: dto.completionStatus,
37
+ });
38
+ }
39
+
40
+ @Post('recreate-tasks')
41
+ @HttpCode(HttpStatus.OK)
42
+ recreateTasks(@Param('id', ParseIntPipe) courseId: number) {
43
+ return this.operationsIntegration.createMissingTasksForCourseLessons(courseId);
44
+ }
45
+ }
@@ -76,6 +76,7 @@ export class CourseStructureService {
76
76
  person: {
77
77
  select: {
78
78
  name: true,
79
+ avatar_id: true,
79
80
  },
80
81
  },
81
82
  },
@@ -171,7 +172,8 @@ export class CourseStructureService {
171
172
  instrutores: lesson.course_lesson_instructor.map((item) => ({
172
173
  id: String(item.instructor_id),
173
174
  role: item.role,
174
- nome: this.personName(item.instructor?.person),
175
+ name: this.personName(item.instructor?.person),
176
+ avatarId: item.instructor?.person?.avatar_id ?? null,
175
177
  })),
176
178
  operationsTaskId: lessonTaskLinks.get(lesson.id)?.taskId ?? null,
177
179
  operationsTaskName: lessonTaskLinks.get(lesson.id)?.taskName ?? null,
@@ -187,6 +189,7 @@ export class CourseStructureService {
187
189
  instructors: instructors.map((instructor) => ({
188
190
  id: instructor.id,
189
191
  name: instructor.name,
192
+ avatarId: instructor.avatarId ?? null,
190
193
  })),
191
194
  curso: courseRecord
192
195
  ? {
@@ -513,6 +516,7 @@ export class CourseStructureService {
513
516
  throw new NotFoundException('Lesson not found for this session');
514
517
  }
515
518
 
519
+ await this.operationsIntegration.deleteTasksForLesson(lessonId, this.prisma);
516
520
  await this.prisma.course_lesson.delete({ where: { id: lessonId } });
517
521
 
518
522
  return { success: true };
@@ -4,6 +4,7 @@ import { QueueModule } from '@hed-hog/queue';
4
4
  import { forwardRef, Module } from '@nestjs/common';
5
5
  import { InstructorModule } from '../instructor/instructor.module';
6
6
  import { CourseLessonController } from './course-lesson.controller';
7
+ import { CourseOperationsController } from './course-operations.controller';
7
8
  import { CourseOperationsIntegrationService } from './course-operations-integration.service';
8
9
  import { CourseStructureController } from './course-structure.controller';
9
10
  import { LmsSettingController } from './lms-setting.controller';
@@ -12,6 +13,7 @@ import { CourseVideoConversionService } from './course-video-conversion.service'
12
13
  import { CourseController } from './course.controller';
13
14
  import { LmsCoursesMcpTools } from './course.mcp-tools';
14
15
  import { CourseService } from './course.service';
16
+ import { LmsOperationsTaskSubscriber } from './lms-operations-task.subscriber';
15
17
 
16
18
  @Module({
17
19
  imports: [
@@ -20,13 +22,14 @@ import { CourseService } from './course.service';
20
22
  forwardRef(() => CoreModule),
21
23
  forwardRef(() => QueueModule),
22
24
  ],
23
- controllers: [CourseController, CourseStructureController, CourseLessonController, LmsSettingController],
25
+ controllers: [CourseController, CourseStructureController, CourseLessonController, LmsSettingController, CourseOperationsController],
24
26
  providers: [
25
27
  CourseOperationsIntegrationService,
26
28
  CourseService,
27
29
  CourseStructureService,
28
30
  CourseVideoConversionService,
29
31
  LmsCoursesMcpTools,
32
+ LmsOperationsTaskSubscriber,
30
33
  ],
31
34
  exports: [
32
35
  forwardRef(() => CourseService),
@@ -0,0 +1,16 @@
1
+ import { IsIn, IsInt, IsOptional, IsPositive, IsString } from 'class-validator';
2
+
3
+ export class UpdateCourseOperationsConfigDto {
4
+ @IsOptional()
5
+ @IsInt()
6
+ @IsPositive()
7
+ projectId?: number | null;
8
+
9
+ @IsOptional()
10
+ @IsIn(['per_lesson', 'per_production_status'])
11
+ taskMode?: 'per_lesson' | 'per_production_status' | null;
12
+
13
+ @IsOptional()
14
+ @IsString()
15
+ completionStatus?: string | null;
16
+ }
@@ -0,0 +1,44 @@
1
+ import { IntegrationDeveloperApiService } from '@hed-hog/core';
2
+ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
3
+ import { PrismaService } from '@hed-hog/api-prisma';
4
+ import { CourseOperationsIntegrationService } from './course-operations-integration.service';
5
+
6
+ @Injectable()
7
+ export class LmsOperationsTaskSubscriber implements OnModuleInit {
8
+ private readonly logger = new Logger(LmsOperationsTaskSubscriber.name);
9
+
10
+ constructor(
11
+ private readonly integrationApi: IntegrationDeveloperApiService,
12
+ private readonly operationsIntegration: CourseOperationsIntegrationService,
13
+ private readonly prisma: PrismaService,
14
+ ) {}
15
+
16
+ onModuleInit(): void {
17
+ this.integrationApi.subscribeMany([
18
+ {
19
+ eventName: 'operations.task.updated',
20
+ consumerName: 'lms.lesson-status-from-task',
21
+ priority: 5,
22
+ handler: async (event) => {
23
+ try {
24
+ const status = event.payload?.status as string | undefined;
25
+ if (status !== 'done') return;
26
+
27
+ const taskId = Number(event.aggregateId);
28
+ if (!taskId || !Number.isInteger(taskId) || taskId <= 0) return;
29
+
30
+ await this.operationsIntegration.applyTaskCompletionToLesson(
31
+ taskId,
32
+ '',
33
+ this.prisma,
34
+ );
35
+ } catch (err) {
36
+ this.logger.error(
37
+ `Error applying task completion to lesson: ${String(err)}`,
38
+ );
39
+ }
40
+ },
41
+ },
42
+ ]);
43
+ }
44
+ }