@hed-hog/lms 0.0.357 → 0.0.361
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-operations-integration.service.d.ts +31 -0
- package/dist/course/course-operations-integration.service.d.ts.map +1 -1
- package/dist/course/course-operations-integration.service.js +286 -22
- package/dist/course/course-operations-integration.service.js.map +1 -1
- package/dist/course/course-operations.controller.d.ts +10 -0
- package/dist/course/course-operations.controller.d.ts.map +1 -0
- package/dist/course/course-operations.controller.js +67 -0
- package/dist/course/course-operations.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +3 -1
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.service.d.ts +3 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +13 -6
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +15 -2
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
- package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-operations-config.dto.js +33 -0
- package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
- package/dist/course/lms-bulk-upload.controller.d.ts +37 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.controller.js +60 -0
- package/dist/course/lms-bulk-upload.controller.js.map +1 -0
- package/dist/course/lms-bulk-upload.service.d.ts +42 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.service.js +169 -0
- package/dist/course/lms-bulk-upload.service.js.map +1 -0
- package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
- package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
- package/dist/course/lms-operations-task.subscriber.js +57 -0
- package/dist/course/lms-operations-task.subscriber.js.map +1 -0
- package/dist/course/lms-setting.controller.d.ts +3 -0
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +9 -1
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.js +1 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +12 -3
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +9 -9
- package/hedhog/data/role.yaml +8 -0
- package/hedhog/data/route.yaml +62 -0
- package/hedhog/data/setting_group.yaml +33 -0
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +31 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +91 -20
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +11 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +21 -88
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
- package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
- package/package.json +7 -7
- package/src/course/course-operations-integration.service.ts +460 -22
- package/src/course/course-operations.controller.ts +45 -0
- package/src/course/course-structure.service.ts +5 -1
- package/src/course/course.module.ts +15 -2
- package/src/course/dto/update-course-operations-config.dto.ts +16 -0
- package/src/course/lms-bulk-upload.controller.ts +27 -0
- package/src/course/lms-bulk-upload.service.ts +204 -0
- package/src/course/lms-operations-task.subscriber.ts +44 -0
- package/src/course/lms-setting.controller.ts +12 -1
- package/src/enterprise/enterprise.service.ts +1 -1
- 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
|
|
437
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -5,13 +5,17 @@ import { forwardRef, Module } from '@nestjs/common';
|
|
|
5
5
|
import { InstructorModule } from '../instructor/instructor.module';
|
|
6
6
|
import { CourseLessonController } from './course-lesson.controller';
|
|
7
7
|
import { CourseOperationsIntegrationService } from './course-operations-integration.service';
|
|
8
|
+
import { CourseOperationsController } from './course-operations.controller';
|
|
8
9
|
import { CourseStructureController } from './course-structure.controller';
|
|
9
|
-
import { LmsSettingController } from './lms-setting.controller';
|
|
10
10
|
import { CourseStructureService } from './course-structure.service';
|
|
11
11
|
import { CourseVideoConversionService } from './course-video-conversion.service';
|
|
12
12
|
import { CourseController } from './course.controller';
|
|
13
13
|
import { LmsCoursesMcpTools } from './course.mcp-tools';
|
|
14
14
|
import { CourseService } from './course.service';
|
|
15
|
+
import { LmsBulkUploadController } from './lms-bulk-upload.controller';
|
|
16
|
+
import { LmsBulkUploadService } from './lms-bulk-upload.service';
|
|
17
|
+
import { LmsOperationsTaskSubscriber } from './lms-operations-task.subscriber';
|
|
18
|
+
import { LmsSettingController } from './lms-setting.controller';
|
|
15
19
|
|
|
16
20
|
@Module({
|
|
17
21
|
imports: [
|
|
@@ -20,13 +24,22 @@ import { CourseService } from './course.service';
|
|
|
20
24
|
forwardRef(() => CoreModule),
|
|
21
25
|
forwardRef(() => QueueModule),
|
|
22
26
|
],
|
|
23
|
-
controllers: [
|
|
27
|
+
controllers: [
|
|
28
|
+
CourseController,
|
|
29
|
+
CourseStructureController,
|
|
30
|
+
CourseLessonController,
|
|
31
|
+
LmsSettingController,
|
|
32
|
+
CourseOperationsController,
|
|
33
|
+
LmsBulkUploadController,
|
|
34
|
+
],
|
|
24
35
|
providers: [
|
|
25
36
|
CourseOperationsIntegrationService,
|
|
26
37
|
CourseService,
|
|
27
38
|
CourseStructureService,
|
|
28
39
|
CourseVideoConversionService,
|
|
40
|
+
LmsBulkUploadService,
|
|
29
41
|
LmsCoursesMcpTools,
|
|
42
|
+
LmsOperationsTaskSubscriber,
|
|
30
43
|
],
|
|
31
44
|
exports: [
|
|
32
45
|
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,27 @@
|
|
|
1
|
+
import { Role, User } from '@hed-hog/api';
|
|
2
|
+
import { Body, Controller, Get, Post } from '@nestjs/common';
|
|
3
|
+
import { LmsBulkUploadService } from './lms-bulk-upload.service';
|
|
4
|
+
|
|
5
|
+
@Role()
|
|
6
|
+
@Controller('lms/bulk-upload')
|
|
7
|
+
export class LmsBulkUploadController {
|
|
8
|
+
constructor(private readonly bulkUploadService: LmsBulkUploadService) {}
|
|
9
|
+
|
|
10
|
+
@Get('settings')
|
|
11
|
+
getSettings() {
|
|
12
|
+
return this.bulkUploadService.getBulkUploadSettings();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Post('temporary-credentials')
|
|
16
|
+
getTemporaryCredentials(@User('id') userId: number) {
|
|
17
|
+
return this.bulkUploadService.getTemporaryCredentials(userId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Post('verify')
|
|
21
|
+
verifyFile(
|
|
22
|
+
@User('id') userId: number,
|
|
23
|
+
@Body() payload: { key?: string; fileName?: string },
|
|
24
|
+
) {
|
|
25
|
+
return this.bulkUploadService.verifyFileOnS3(userId, payload ?? {});
|
|
26
|
+
}
|
|
27
|
+
}
|