@hed-hog/lms 0.0.361 → 0.0.365
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/bitcode-wallet/bitcode-wallet.service.d.ts +66 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +91 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
- package/dist/class-group/class-group.controller.d.ts +16 -16
- package/dist/class-group/class-group.service.d.ts +12 -12
- package/dist/course/course-audio-transcription.service.d.ts +3 -2
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +49 -8
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-lesson.controller.d.ts +4 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +10 -0
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +24 -9
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +30 -3
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +25 -3
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +234 -24
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +8 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +87 -51
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course-video-hls.service.d.ts +57 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +767 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +115 -11
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +66 -28
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +13 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +112 -11
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +682 -72
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js +34 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
- package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +5 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.js +26 -0
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts +54 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.js +537 -0
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts +32 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.js +301 -0
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
- package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
- package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.constants.js +7 -0
- package/dist/course/lms-bulk-upload.constants.js.map +1 -0
- package/dist/course/lms-bulk-upload.controller.d.ts +144 -1
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +114 -4
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +153 -3
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +659 -21
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +6 -2
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +25 -8
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +20 -20
- package/dist/enterprise/enterprise.service.d.ts +20 -20
- package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
- package/dist/enterprise/training/training-admin.service.d.ts +11 -11
- package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
- package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
- package/dist/enterprise/training/training-student.controller.d.ts +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +52 -1
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
- package/dist/evaluation/evaluation.controller.d.ts +8 -8
- package/dist/evaluation/evaluation.service.d.ts +26 -8
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +30 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +440 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +21 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
- package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
- package/dist/platforma/dto/update-profile.dto.js +87 -0
- package/dist/platforma/dto/update-profile.dto.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +182 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +243 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/platforma/platforma.service.d.ts +27 -0
- package/dist/platforma/platforma.service.d.ts.map +1 -0
- package/dist/platforma/platforma.service.js +274 -0
- package/dist/platforma/platforma.service.js.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +56 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
- package/dist/student-xp/student-xp.controller.js +138 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -0
- package/dist/student-xp/student-xp.module.d.ts +3 -0
- package/dist/student-xp/student-xp.module.d.ts.map +1 -0
- package/dist/student-xp/student-xp.module.js +25 -0
- package/dist/student-xp/student-xp.module.js.map +1 -0
- package/dist/student-xp/student-xp.service.d.ts +81 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -0
- package/dist/student-xp/student-xp.service.js +247 -0
- package/dist/student-xp/student-xp.service.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
- package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.controller.js +105 -0
- package/dist/xp-catalog/xp-area.controller.js.map +1 -0
- package/dist/xp-catalog/xp-area.service.d.ts +35 -0
- package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.service.js +168 -0
- package/dist/xp-catalog/xp-area.service.js.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.js +29 -0
- package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
- package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.js +146 -0
- package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.js +113 -0
- package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
- package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
- package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.service.js +174 -0
- package/dist/xp-catalog/xp-skill.service.js.map +1 -0
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +91 -7
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +511 -29
- package/hedhog/data/setting_group.yaml +20 -20
- package/hedhog/data/xp_area.yaml +164 -0
- package/hedhog/data/xp_learning_type.yaml +131 -0
- package/hedhog/data/xp_skill.yaml +1834 -0
- package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1749 -0
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
- package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +73 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1172 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +87 -46
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +618 -480
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +672 -737
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +24 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +53 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +80 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
- package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
- package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +445 -230
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
- package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
- package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
- package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
- package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
- package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
- package/hedhog/frontend/messages/en.json +412 -31
- package/hedhog/frontend/messages/pt.json +412 -31
- package/hedhog/table/course_export.yaml +62 -0
- package/hedhog/table/lesson_xp_map.yaml +50 -0
- package/hedhog/table/lesson_xp_segment.yaml +40 -0
- package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
- package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
- package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
- package/hedhog/table/student_area_xp.yaml +30 -0
- package/hedhog/table/student_learning_type_xp.yaml +30 -0
- package/hedhog/table/student_skill_xp.yaml +30 -0
- package/hedhog/table/student_xp_event.yaml +34 -0
- package/hedhog/table/xp_area.yaml +39 -0
- package/hedhog/table/xp_learning_type.yaml +34 -0
- package/hedhog/table/xp_skill.yaml +39 -0
- package/package.json +13 -8
- package/src/bitcode-wallet/bitcode-wallet.service.ts +152 -0
- package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
- package/src/course/course-audio-transcription.service.ts +58 -21
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-lesson.controller.ts +6 -1
- package/src/course/course-structure.controller.ts +23 -1
- package/src/course/course-structure.service.ts +273 -7
- package/src/course/course-video-conversion.service.ts +113 -75
- package/src/course/course-video-hls.service.ts +946 -0
- package/src/course/course.controller.ts +54 -21
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +13 -0
- package/src/course/course.service.ts +906 -76
- package/src/course/dto/cleanup-course-storage.dto.ts +23 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
- package/src/course/dto/create-course-bulk-job.dto.ts +10 -0
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/lms-bulk-upload-automation.service.ts +707 -0
- package/src/course/lms-bulk-upload-infra.service.ts +360 -0
- package/src/course/lms-bulk-upload.constants.ts +5 -0
- package/src/course/lms-bulk-upload.controller.ts +110 -4
- package/src/course/lms-bulk-upload.service.ts +1092 -204
- package/src/course/lms-setting.controller.ts +26 -8
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
- package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +570 -0
- package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
- package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
- package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
- package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +21 -5
- package/src/platforma/dto/update-profile.dto.ts +59 -0
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +152 -3
- package/src/platforma/platforma.service.ts +268 -0
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +92 -0
- package/src/student-xp/student-xp.module.ts +12 -0
- package/src/student-xp/student-xp.service.ts +318 -0
- package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
- package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
- package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
- package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
- package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
- package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
- package/src/xp-catalog/xp-area.controller.ts +64 -0
- package/src/xp-catalog/xp-area.service.ts +196 -0
- package/src/xp-catalog/xp-catalog.module.ts +16 -0
- package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
- package/src/xp-catalog/xp-learning-type.service.ts +170 -0
- package/src/xp-catalog/xp-skill.controller.ts +71 -0
- package/src/xp-catalog/xp-skill.service.ts +205 -0
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
FileService,
|
|
4
|
+
IntegrationDeveloperApiService,
|
|
5
|
+
NotificationService,
|
|
6
|
+
} from '@hed-hog/core';
|
|
7
|
+
import { IJobHandler, QueueHandlerRegistry, QueueJobService } from '@hed-hog/queue';
|
|
8
|
+
import {
|
|
9
|
+
BadRequestException,
|
|
10
|
+
Inject,
|
|
11
|
+
Injectable,
|
|
12
|
+
Logger,
|
|
13
|
+
NotFoundException,
|
|
14
|
+
OnModuleInit,
|
|
15
|
+
forwardRef,
|
|
16
|
+
} from '@nestjs/common';
|
|
4
17
|
import { CourseOperationsIntegrationService } from './course-operations-integration.service';
|
|
18
|
+
import {
|
|
19
|
+
COURSE_STORAGE_CLEANUP_CATEGORIES,
|
|
20
|
+
type CourseStorageCleanupCategory,
|
|
21
|
+
} from './dto/cleanup-course-storage.dto';
|
|
5
22
|
import { CreateCourseDto } from './dto/create-course.dto';
|
|
6
23
|
import { UpdateCourseDto } from './dto/update-course.dto';
|
|
7
24
|
|
|
25
|
+
export const LMS_COURSE_DELETE_JOB = 'lms.course.delete';
|
|
26
|
+
export const LMS_COURSE_STORAGE_CLEANUP_JOB = 'lms.course.storage.cleanup';
|
|
27
|
+
|
|
28
|
+
type CourseStorageRow = {
|
|
29
|
+
file_id: number | null;
|
|
30
|
+
file_size: number | null;
|
|
31
|
+
storage_category: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
8
34
|
type CourseImageTypeSlug = 'course-logo' | 'course-banner';
|
|
9
35
|
|
|
10
36
|
type CourseExtraFields = {
|
|
@@ -30,15 +56,249 @@ type PersistCourseExtrasInput = {
|
|
|
30
56
|
};
|
|
31
57
|
|
|
32
58
|
@Injectable()
|
|
33
|
-
export class CourseService {
|
|
59
|
+
export class CourseService implements OnModuleInit, IJobHandler {
|
|
60
|
+
private readonly logger = new Logger(CourseService.name);
|
|
61
|
+
|
|
34
62
|
constructor(
|
|
35
63
|
private readonly prisma: PrismaService,
|
|
36
64
|
private readonly fileService: FileService,
|
|
37
65
|
@Inject(forwardRef(() => IntegrationDeveloperApiService))
|
|
38
66
|
private readonly integrationApi: IntegrationDeveloperApiService,
|
|
67
|
+
@Inject(forwardRef(() => QueueJobService))
|
|
68
|
+
private readonly queueJobService: QueueJobService,
|
|
69
|
+
@Inject(forwardRef(() => QueueHandlerRegistry))
|
|
70
|
+
private readonly queueRegistry: QueueHandlerRegistry,
|
|
71
|
+
@Inject(forwardRef(() => NotificationService))
|
|
72
|
+
private readonly notificationService: NotificationService,
|
|
39
73
|
private readonly operationsIntegration: CourseOperationsIntegrationService,
|
|
40
74
|
) {}
|
|
41
75
|
|
|
76
|
+
onModuleInit() {
|
|
77
|
+
this.queueRegistry.register(LMS_COURSE_DELETE_JOB, this);
|
|
78
|
+
this.queueRegistry.register(LMS_COURSE_STORAGE_CLEANUP_JOB, this);
|
|
79
|
+
this.logger.log(`Registered handler for "${LMS_COURSE_DELETE_JOB}"`);
|
|
80
|
+
this.logger.log(`Registered handler for "${LMS_COURSE_STORAGE_CLEANUP_JOB}"`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async handle(job: {
|
|
84
|
+
id: number;
|
|
85
|
+
type: string;
|
|
86
|
+
queue_name: string;
|
|
87
|
+
payload: Record<string, any>;
|
|
88
|
+
attempts: number;
|
|
89
|
+
max_attempts: number;
|
|
90
|
+
source_module?: string | null;
|
|
91
|
+
source_entity?: string | null;
|
|
92
|
+
source_entity_id?: string | null;
|
|
93
|
+
}) {
|
|
94
|
+
if (job.type === LMS_COURSE_STORAGE_CLEANUP_JOB) {
|
|
95
|
+
return this.handleStorageCleanupJob(job);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (job.type === LMS_COURSE_DELETE_JOB) {
|
|
99
|
+
return this.handleCourseDeleteJob(job);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new BadRequestException(`Unsupported job type: ${job.type}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async handleCourseDeleteJob(job: {
|
|
106
|
+
id: number;
|
|
107
|
+
type: string;
|
|
108
|
+
queue_name: string;
|
|
109
|
+
payload: Record<string, any>;
|
|
110
|
+
attempts: number;
|
|
111
|
+
max_attempts: number;
|
|
112
|
+
source_module?: string | null;
|
|
113
|
+
source_entity?: string | null;
|
|
114
|
+
source_entity_id?: string | null;
|
|
115
|
+
}) {
|
|
116
|
+
const { courseId, notificationId, notificationUserId } = job.payload as {
|
|
117
|
+
courseId?: number;
|
|
118
|
+
notificationId?: number;
|
|
119
|
+
notificationUserId?: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (!courseId) {
|
|
123
|
+
throw new BadRequestException('Missing courseId in job payload');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const notify = async (
|
|
127
|
+
progress: number,
|
|
128
|
+
body: string,
|
|
129
|
+
success?: boolean,
|
|
130
|
+
) => {
|
|
131
|
+
if (!notificationId || !notificationUserId) return;
|
|
132
|
+
|
|
133
|
+
await this.notificationService
|
|
134
|
+
.updateProgress(notificationUserId, notificationId, {
|
|
135
|
+
progress,
|
|
136
|
+
body,
|
|
137
|
+
...(success !== undefined ? { success } : {}),
|
|
138
|
+
})
|
|
139
|
+
.catch(() => undefined);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await notify(5, 'Preparando exclusão completa do curso...');
|
|
144
|
+
|
|
145
|
+
const course = await this.prisma.course.findUnique({
|
|
146
|
+
where: { id: courseId },
|
|
147
|
+
select: { id: true, status: true, title: true, slug: true },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!course) {
|
|
151
|
+
throw new NotFoundException('Course not found');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (course.status !== 'archived') {
|
|
155
|
+
throw new BadRequestException('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const impact = await this.getCourseDeletionImpact(courseId);
|
|
159
|
+
await notify(
|
|
160
|
+
20,
|
|
161
|
+
`Removendo ${impact.deletionImpact.fileCount} arquivo(s) relacionados ao curso...`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (impact.deletionImpact.fileIds.length > 0) {
|
|
165
|
+
await this.fileService
|
|
166
|
+
.delete('pt', { ids: impact.deletionImpact.fileIds })
|
|
167
|
+
.catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await notify(75, 'Removendo registros do curso e dependências...');
|
|
171
|
+
|
|
172
|
+
await this.prisma.course.delete({ where: { id: courseId } });
|
|
173
|
+
|
|
174
|
+
await notify(90, 'Finalizando exclusão e publicando evento...');
|
|
175
|
+
|
|
176
|
+
await this.integrationApi
|
|
177
|
+
.publishEvent({
|
|
178
|
+
eventName: 'lms.course.deleted',
|
|
179
|
+
sourceModule: 'lms',
|
|
180
|
+
aggregateType: 'course',
|
|
181
|
+
aggregateId: String(courseId),
|
|
182
|
+
payload: { id: courseId },
|
|
183
|
+
})
|
|
184
|
+
.catch(() => null);
|
|
185
|
+
|
|
186
|
+
await notify(100, 'Curso e todos os arquivos relacionados foram excluídos.', true);
|
|
187
|
+
|
|
188
|
+
return { success: true, courseId };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const message =
|
|
191
|
+
error instanceof Error && error.message
|
|
192
|
+
? error.message
|
|
193
|
+
: 'Falha ao excluir o curso.';
|
|
194
|
+
await notify(100, message, false);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async handleStorageCleanupJob(job: {
|
|
200
|
+
id: number;
|
|
201
|
+
type: string;
|
|
202
|
+
queue_name: string;
|
|
203
|
+
payload: Record<string, any>;
|
|
204
|
+
attempts: number;
|
|
205
|
+
max_attempts: number;
|
|
206
|
+
source_module?: string | null;
|
|
207
|
+
source_entity?: string | null;
|
|
208
|
+
source_entity_id?: string | null;
|
|
209
|
+
}) {
|
|
210
|
+
const { courseId, category, notificationId, notificationUserId } =
|
|
211
|
+
job.payload as {
|
|
212
|
+
courseId?: number;
|
|
213
|
+
category?: string;
|
|
214
|
+
notificationId?: number;
|
|
215
|
+
notificationUserId?: number;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (!courseId) {
|
|
219
|
+
throw new BadRequestException('Missing courseId in job payload');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!this.isSupportedStorageCleanupCategory(category)) {
|
|
223
|
+
throw new BadRequestException('Invalid storage category');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const notify = async (
|
|
227
|
+
progress: number,
|
|
228
|
+
body: string,
|
|
229
|
+
success?: boolean,
|
|
230
|
+
) => {
|
|
231
|
+
if (!notificationId || !notificationUserId) return;
|
|
232
|
+
|
|
233
|
+
await this.notificationService
|
|
234
|
+
.updateProgress(notificationUserId, notificationId, {
|
|
235
|
+
progress,
|
|
236
|
+
body,
|
|
237
|
+
...(success !== undefined ? { success } : {}),
|
|
238
|
+
})
|
|
239
|
+
.catch(() => undefined);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const categoryLabel = this.getStorageCategoryLabel(category);
|
|
244
|
+
await notify(5, `Preparando limpeza da categoria ${categoryLabel}...`);
|
|
245
|
+
|
|
246
|
+
const impact = await this.getCourseStorageCategoryImpact(courseId, category);
|
|
247
|
+
|
|
248
|
+
if (impact.fileCount === 0) {
|
|
249
|
+
await notify(100, `Nenhum arquivo encontrado em ${categoryLabel}.`, true);
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
status: 'empty',
|
|
253
|
+
courseId,
|
|
254
|
+
category,
|
|
255
|
+
cleanupImpact: impact,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await notify(
|
|
260
|
+
25,
|
|
261
|
+
`Removendo vínculos da categoria ${categoryLabel} (${impact.fileCount} arquivo(s))...`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await this.deleteCourseStorageCategoryLinks(courseId, category);
|
|
265
|
+
|
|
266
|
+
await notify(
|
|
267
|
+
65,
|
|
268
|
+
`Excluindo ${impact.fileCount} arquivo(s) da categoria ${categoryLabel}...`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (impact.fileIds.length > 0) {
|
|
272
|
+
await this.fileService.delete('pt', { ids: impact.fileIds }).catch(() => undefined);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await notify(
|
|
276
|
+
100,
|
|
277
|
+
`Categoria ${categoryLabel} limpa com sucesso (${this.formatBytes(impact.totalBytes)} liberados).`,
|
|
278
|
+
true,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
status: 'processed',
|
|
284
|
+
courseId,
|
|
285
|
+
category,
|
|
286
|
+
cleanupImpact: {
|
|
287
|
+
fileCount: impact.fileCount,
|
|
288
|
+
totalBytes: impact.totalBytes,
|
|
289
|
+
formattedSize: this.formatBytes(impact.totalBytes),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const message =
|
|
294
|
+
error instanceof Error && error.message
|
|
295
|
+
? error.message
|
|
296
|
+
: 'Falha ao limpar a categoria de armazenamento.';
|
|
297
|
+
await notify(100, message, false);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
42
302
|
private normalizeLevel(value?: string | null) {
|
|
43
303
|
if (!value) return undefined;
|
|
44
304
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -220,10 +480,12 @@ export class CourseService {
|
|
|
220
480
|
]);
|
|
221
481
|
|
|
222
482
|
const courseIds = courses.map((course) => course.id);
|
|
223
|
-
const [extrasById, projectLinksById] =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
483
|
+
const [extrasById, projectLinksById, coursesWithRunningJobs] =
|
|
484
|
+
await Promise.all([
|
|
485
|
+
this.getCourseExtras(courseIds),
|
|
486
|
+
this.operationsIntegration.getCourseProjectLinks(courseIds),
|
|
487
|
+
this.getCoursesWithRunningJobs(courseIds),
|
|
488
|
+
]);
|
|
227
489
|
|
|
228
490
|
return {
|
|
229
491
|
total,
|
|
@@ -236,6 +498,8 @@ export class CourseService {
|
|
|
236
498
|
undefined,
|
|
237
499
|
extrasById.get(c.id),
|
|
238
500
|
projectLinksById.get(c.id),
|
|
501
|
+
undefined,
|
|
502
|
+
coursesWithRunningJobs.has(c.id),
|
|
239
503
|
),
|
|
240
504
|
),
|
|
241
505
|
};
|
|
@@ -285,7 +549,16 @@ export class CourseService {
|
|
|
285
549
|
},
|
|
286
550
|
},
|
|
287
551
|
file: {
|
|
288
|
-
select: {
|
|
552
|
+
select: {
|
|
553
|
+
id: true,
|
|
554
|
+
filename: true,
|
|
555
|
+
size: true,
|
|
556
|
+
file_mimetype: {
|
|
557
|
+
select: {
|
|
558
|
+
name: true,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
289
562
|
},
|
|
290
563
|
},
|
|
291
564
|
orderBy: [{ is_primary: 'desc' }, { order: 'asc' }],
|
|
@@ -386,7 +659,7 @@ export class CourseService {
|
|
|
386
659
|
};
|
|
387
660
|
});
|
|
388
661
|
|
|
389
|
-
|
|
662
|
+
const mapped = this.mapCourse(
|
|
390
663
|
c,
|
|
391
664
|
{
|
|
392
665
|
lessonCount,
|
|
@@ -399,6 +672,19 @@ export class CourseService {
|
|
|
399
672
|
projectLinksById.get(id),
|
|
400
673
|
lessonInstructors,
|
|
401
674
|
);
|
|
675
|
+
|
|
676
|
+
const deletionImpact = await this.getCourseDeletionImpact(id);
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
...mapped,
|
|
680
|
+
deletionImpact: {
|
|
681
|
+
fileCount: deletionImpact.deletionImpact.fileCount,
|
|
682
|
+
totalBytes: deletionImpact.deletionImpact.totalBytes,
|
|
683
|
+
formattedSize: this.formatBytes(deletionImpact.deletionImpact.totalBytes),
|
|
684
|
+
countsBySource: deletionImpact.deletionImpact.countsBySource,
|
|
685
|
+
enrollmentCount: mapped.enrollmentCount ?? 0,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
402
688
|
}
|
|
403
689
|
|
|
404
690
|
async create(dto: CreateCourseDto) {
|
|
@@ -713,10 +999,10 @@ export class CourseService {
|
|
|
713
999
|
return updateResult;
|
|
714
1000
|
}
|
|
715
1001
|
|
|
716
|
-
async remove(id: number) {
|
|
1002
|
+
async remove(id: number, userId?: number | null) {
|
|
717
1003
|
const course = await this.prisma.course.findUnique({
|
|
718
1004
|
where: { id },
|
|
719
|
-
select: { id: true, status: true },
|
|
1005
|
+
select: { id: true, status: true, title: true, slug: true },
|
|
720
1006
|
});
|
|
721
1007
|
|
|
722
1008
|
if (!course) {
|
|
@@ -727,69 +1013,465 @@ export class CourseService {
|
|
|
727
1013
|
throw new BadRequestException('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED');
|
|
728
1014
|
}
|
|
729
1015
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
await this.fileService.delete('pt', { ids: fileIds }).catch(() => undefined);
|
|
1016
|
+
if (!userId) {
|
|
1017
|
+
throw new BadRequestException('Authenticated user is required');
|
|
733
1018
|
}
|
|
734
1019
|
|
|
735
|
-
await this.
|
|
1020
|
+
const impact = await this.getCourseDeletionImpact(id);
|
|
1021
|
+
|
|
1022
|
+
const notification = await this.notificationService.create({
|
|
1023
|
+
user_id: userId,
|
|
1024
|
+
title: `Excluindo curso "${course.title ?? course.slug}"`,
|
|
1025
|
+
body: `Preparando remoção de ${impact.deletionImpact.fileCount} arquivo(s) (${this.formatBytes(impact.deletionImpact.totalBytes)}).`,
|
|
1026
|
+
type: 'progress' as any,
|
|
1027
|
+
progress: 1,
|
|
1028
|
+
started_at: new Date().toISOString(),
|
|
1029
|
+
auto_remove: false,
|
|
1030
|
+
action_type: 'url' as any,
|
|
1031
|
+
action_url: '/lms/courses',
|
|
1032
|
+
action_data: {
|
|
1033
|
+
source: 'lms-course-delete',
|
|
1034
|
+
courseId: id,
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
736
1037
|
|
|
737
|
-
await this.
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1038
|
+
const job = await this.queueJobService.enqueue(
|
|
1039
|
+
{
|
|
1040
|
+
type: LMS_COURSE_DELETE_JOB,
|
|
1041
|
+
queueName: LMS_COURSE_DELETE_JOB,
|
|
1042
|
+
payload: {
|
|
1043
|
+
courseId: id,
|
|
1044
|
+
notificationId: notification.id,
|
|
1045
|
+
notificationUserId: userId,
|
|
1046
|
+
},
|
|
1047
|
+
sourceModule: 'lms',
|
|
1048
|
+
sourceEntity: 'course',
|
|
1049
|
+
sourceEntityId: String(id),
|
|
1050
|
+
maxAttempts: 3,
|
|
1051
|
+
},
|
|
1052
|
+
userId,
|
|
1053
|
+
);
|
|
744
1054
|
|
|
745
|
-
return {
|
|
1055
|
+
return {
|
|
1056
|
+
success: true,
|
|
1057
|
+
status: 'queued',
|
|
1058
|
+
queueJobId: job.id,
|
|
1059
|
+
notificationId: notification.id,
|
|
1060
|
+
deletionImpact: {
|
|
1061
|
+
fileCount: impact.deletionImpact.fileCount,
|
|
1062
|
+
totalBytes: impact.deletionImpact.totalBytes,
|
|
1063
|
+
formattedSize: this.formatBytes(impact.deletionImpact.totalBytes),
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
746
1066
|
}
|
|
747
1067
|
|
|
748
|
-
|
|
1068
|
+
async enqueueStorageCategoryCleanup(
|
|
1069
|
+
courseId: number,
|
|
1070
|
+
category: CourseStorageCleanupCategory,
|
|
1071
|
+
userId?: number | null,
|
|
1072
|
+
) {
|
|
1073
|
+
if (!userId) {
|
|
1074
|
+
throw new BadRequestException('Authenticated user is required');
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (!this.isSupportedStorageCleanupCategory(category)) {
|
|
1078
|
+
throw new BadRequestException('Invalid storage category');
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const course = await this.prisma.course.findUnique({
|
|
1082
|
+
where: { id: courseId },
|
|
1083
|
+
select: { id: true, title: true, slug: true },
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
if (!course) {
|
|
1087
|
+
throw new NotFoundException('Course not found');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const categoryLabel = this.getStorageCategoryLabel(category);
|
|
1091
|
+
const impact = await this.getCourseStorageCategoryImpact(courseId, category);
|
|
1092
|
+
|
|
1093
|
+
if (impact.fileCount === 0) {
|
|
1094
|
+
return {
|
|
1095
|
+
success: true,
|
|
1096
|
+
status: 'empty',
|
|
1097
|
+
cleanupImpact: {
|
|
1098
|
+
fileCount: 0,
|
|
1099
|
+
totalBytes: 0,
|
|
1100
|
+
formattedSize: this.formatBytes(0),
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const notification = await this.notificationService.create({
|
|
1106
|
+
user_id: userId,
|
|
1107
|
+
title: `Limpando armazenamento (${categoryLabel})`,
|
|
1108
|
+
body: `Preparando remoção de ${impact.fileCount} arquivo(s) (${this.formatBytes(impact.totalBytes)}).`,
|
|
1109
|
+
type: 'progress' as any,
|
|
1110
|
+
progress: 1,
|
|
1111
|
+
started_at: new Date().toISOString(),
|
|
1112
|
+
auto_remove: false,
|
|
1113
|
+
action_type: 'url' as any,
|
|
1114
|
+
action_url: `/lms/courses/${courseId}`,
|
|
1115
|
+
action_data: {
|
|
1116
|
+
source: 'lms-course-storage-cleanup',
|
|
1117
|
+
courseId,
|
|
1118
|
+
category,
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
const job = await this.queueJobService.enqueue(
|
|
1123
|
+
{
|
|
1124
|
+
type: LMS_COURSE_STORAGE_CLEANUP_JOB,
|
|
1125
|
+
queueName: LMS_COURSE_STORAGE_CLEANUP_JOB,
|
|
1126
|
+
payload: {
|
|
1127
|
+
courseId,
|
|
1128
|
+
category,
|
|
1129
|
+
notificationId: notification.id,
|
|
1130
|
+
notificationUserId: userId,
|
|
1131
|
+
},
|
|
1132
|
+
sourceModule: 'lms',
|
|
1133
|
+
sourceEntity: 'course',
|
|
1134
|
+
sourceEntityId: String(courseId),
|
|
1135
|
+
maxAttempts: 3,
|
|
1136
|
+
},
|
|
1137
|
+
userId,
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
success: true,
|
|
1142
|
+
status: 'queued',
|
|
1143
|
+
queueJobId: job.id,
|
|
1144
|
+
notificationId: notification.id,
|
|
1145
|
+
category,
|
|
1146
|
+
cleanupImpact: {
|
|
1147
|
+
fileCount: impact.fileCount,
|
|
1148
|
+
totalBytes: impact.totalBytes,
|
|
1149
|
+
formattedSize: this.formatBytes(impact.totalBytes),
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private isSupportedStorageCleanupCategory(
|
|
1155
|
+
value: unknown,
|
|
1156
|
+
): value is CourseStorageCleanupCategory {
|
|
1157
|
+
return (
|
|
1158
|
+
typeof value === 'string' &&
|
|
1159
|
+
COURSE_STORAGE_CLEANUP_CATEGORIES.includes(
|
|
1160
|
+
value as CourseStorageCleanupCategory,
|
|
1161
|
+
)
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
private getStorageCategoryLabel(category: CourseStorageCleanupCategory) {
|
|
1166
|
+
const labels: Record<CourseStorageCleanupCategory, string> = {
|
|
1167
|
+
video_original: 'Vídeos originais',
|
|
1168
|
+
video_profile: 'Vídeos convertidos',
|
|
1169
|
+
lesson_audio: 'Áudios',
|
|
1170
|
+
extracted_image: 'Imagens extraídas',
|
|
1171
|
+
student_download: 'Downloads do aluno',
|
|
1172
|
+
supplementary_material: 'Materiais de apoio',
|
|
1173
|
+
course_image: 'Imagens do curso',
|
|
1174
|
+
course_file: 'Arquivos do curso',
|
|
1175
|
+
other_lesson_file: 'Outros arquivos',
|
|
1176
|
+
course_export: 'Arquivos de exportação',
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
return labels[category];
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private async getCourseStorageCategoryImpact(
|
|
1183
|
+
courseId: number,
|
|
1184
|
+
category: CourseStorageCleanupCategory,
|
|
1185
|
+
) {
|
|
1186
|
+
const rows = await this.getCourseStorageRows(courseId);
|
|
1187
|
+
|
|
1188
|
+
const uniqueFiles = new Map<number, number>();
|
|
1189
|
+
for (const row of rows) {
|
|
1190
|
+
const rowCategory = String(row.storage_category || 'other_lesson_file');
|
|
1191
|
+
if (rowCategory !== category) continue;
|
|
1192
|
+
|
|
1193
|
+
const fileId = Number(row.file_id ?? 0);
|
|
1194
|
+
if (fileId <= 0) continue;
|
|
1195
|
+
|
|
1196
|
+
if (!uniqueFiles.has(fileId)) {
|
|
1197
|
+
uniqueFiles.set(fileId, Number(row.file_size ?? 0) || 0);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const fileIds = Array.from(uniqueFiles.keys());
|
|
1202
|
+
const totalBytes = Array.from(uniqueFiles.values()).reduce(
|
|
1203
|
+
(sum, size) => sum + size,
|
|
1204
|
+
0,
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
return {
|
|
1208
|
+
category,
|
|
1209
|
+
fileIds,
|
|
1210
|
+
fileCount: fileIds.length,
|
|
1211
|
+
totalBytes,
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
private async deleteCourseStorageCategoryLinks(
|
|
1216
|
+
courseId: number,
|
|
1217
|
+
category: CourseStorageCleanupCategory,
|
|
1218
|
+
) {
|
|
1219
|
+
if (category === 'course_image') {
|
|
1220
|
+
await this.prisma.course_image.deleteMany({ where: { course_id: courseId } });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (category === 'course_file') {
|
|
1225
|
+
await this.prisma.course_file.deleteMany({ where: { course_id: courseId } });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (category === 'extracted_image') {
|
|
1230
|
+
await this.prisma.$executeRaw`
|
|
1231
|
+
DELETE FROM course_lesson_video_frame clvf
|
|
1232
|
+
USING course_lesson cl, course_module cm
|
|
1233
|
+
WHERE clvf.course_lesson_id = cl.id
|
|
1234
|
+
AND cl.course_module_id = cm.id
|
|
1235
|
+
AND cm.course_id = ${courseId}
|
|
1236
|
+
`;
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (category === 'video_profile') {
|
|
1241
|
+
await this.prisma.$executeRaw`
|
|
1242
|
+
DELETE FROM course_lesson_file clf
|
|
1243
|
+
USING course_lesson cl, course_module cm
|
|
1244
|
+
WHERE clf.course_lesson_id = cl.id
|
|
1245
|
+
AND cl.course_module_id = cm.id
|
|
1246
|
+
AND cm.course_id = ${courseId}
|
|
1247
|
+
AND clf.type LIKE 'video_profile:%'
|
|
1248
|
+
`;
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (category === 'other_lesson_file') {
|
|
1253
|
+
await this.prisma.$executeRaw`
|
|
1254
|
+
DELETE FROM course_lesson_file clf
|
|
1255
|
+
USING course_lesson cl, course_module cm
|
|
1256
|
+
WHERE clf.course_lesson_id = cl.id
|
|
1257
|
+
AND cl.course_module_id = cm.id
|
|
1258
|
+
AND cm.course_id = ${courseId}
|
|
1259
|
+
AND (
|
|
1260
|
+
clf.type IS NULL
|
|
1261
|
+
OR (
|
|
1262
|
+
clf.type <> 'video_original'
|
|
1263
|
+
AND clf.type <> 'lesson_audio'
|
|
1264
|
+
AND clf.type <> 'student_download'
|
|
1265
|
+
AND clf.type <> 'supplementary_material'
|
|
1266
|
+
AND clf.type NOT LIKE 'video_profile:%'
|
|
1267
|
+
)
|
|
1268
|
+
)
|
|
1269
|
+
`;
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (category === 'course_export') {
|
|
1274
|
+
const exports = await this.prisma.course_export.findMany({
|
|
1275
|
+
where: {
|
|
1276
|
+
course_id: courseId,
|
|
1277
|
+
file_id: { not: null },
|
|
1278
|
+
status: { in: ['completed', 'failed'] },
|
|
1279
|
+
},
|
|
1280
|
+
select: { id: true, file_id: true },
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
const fileIds = exports
|
|
1284
|
+
.map((e) => e.file_id)
|
|
1285
|
+
.filter((id): id is number => id != null);
|
|
1286
|
+
|
|
1287
|
+
await this.prisma.course_export.deleteMany({
|
|
1288
|
+
where: { id: { in: exports.map((e) => e.id) } },
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
if (fileIds.length > 0) {
|
|
1292
|
+
await this.fileService.delete('pt', { ids: fileIds }).catch(() => undefined);
|
|
1293
|
+
}
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
await this.prisma.course_lesson_file.deleteMany({
|
|
1298
|
+
where: {
|
|
1299
|
+
course_lesson: { course_module: { course_id: courseId } },
|
|
1300
|
+
type: category,
|
|
1301
|
+
},
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
private async getCourseStorageRows(courseId: number) {
|
|
1306
|
+
return this.prisma.$queryRaw<CourseStorageRow[]>`
|
|
1307
|
+
SELECT files.file_id, files.file_size, files.storage_category
|
|
1308
|
+
FROM (
|
|
1309
|
+
SELECT ci.file_id, f.size AS file_size, 'course_image' AS storage_category
|
|
1310
|
+
FROM course_image ci
|
|
1311
|
+
INNER JOIN file f ON f.id = ci.file_id
|
|
1312
|
+
WHERE ci.course_id = ${courseId}
|
|
1313
|
+
|
|
1314
|
+
UNION ALL
|
|
1315
|
+
|
|
1316
|
+
SELECT cf.file_id, f.size AS file_size, 'course_file' AS storage_category
|
|
1317
|
+
FROM course_file cf
|
|
1318
|
+
INNER JOIN file f ON f.id = cf.file_id
|
|
1319
|
+
WHERE cf.course_id = ${courseId}
|
|
1320
|
+
|
|
1321
|
+
UNION ALL
|
|
1322
|
+
|
|
1323
|
+
SELECT clf.file_id,
|
|
1324
|
+
f.size AS file_size,
|
|
1325
|
+
CASE
|
|
1326
|
+
WHEN clf.type = 'video_original' THEN 'video_original'
|
|
1327
|
+
WHEN clf.type LIKE 'video_profile:%' THEN 'video_profile'
|
|
1328
|
+
WHEN clf.type = 'lesson_audio' THEN 'lesson_audio'
|
|
1329
|
+
WHEN clf.type = 'student_download' THEN 'student_download'
|
|
1330
|
+
WHEN clf.type = 'supplementary_material' THEN 'supplementary_material'
|
|
1331
|
+
ELSE 'other_lesson_file'
|
|
1332
|
+
END AS storage_category
|
|
1333
|
+
FROM course_lesson_file clf
|
|
1334
|
+
INNER JOIN file f ON f.id = clf.file_id
|
|
1335
|
+
INNER JOIN course_lesson cl ON cl.id = clf.course_lesson_id
|
|
1336
|
+
INNER JOIN course_module cm ON cm.id = cl.course_module_id
|
|
1337
|
+
WHERE cm.course_id = ${courseId}
|
|
1338
|
+
|
|
1339
|
+
UNION ALL
|
|
1340
|
+
|
|
1341
|
+
SELECT clvf.file_id, f.size AS file_size, 'extracted_image' AS storage_category
|
|
1342
|
+
FROM course_lesson_video_frame clvf
|
|
1343
|
+
INNER JOIN file f ON f.id = clvf.file_id
|
|
1344
|
+
INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
|
|
1345
|
+
INNER JOIN course_module cm ON cm.id = cl.course_module_id
|
|
1346
|
+
WHERE cm.course_id = ${courseId}
|
|
1347
|
+
|
|
1348
|
+
UNION ALL
|
|
1349
|
+
|
|
1350
|
+
SELECT ce.file_id, f.size AS file_size, 'course_export' AS storage_category
|
|
1351
|
+
FROM course_export ce
|
|
1352
|
+
INNER JOIN file f ON f.id = ce.file_id
|
|
1353
|
+
WHERE ce.course_id = ${courseId}
|
|
1354
|
+
AND ce.status = 'completed'
|
|
1355
|
+
AND ce.file_id IS NOT NULL
|
|
1356
|
+
) AS files
|
|
1357
|
+
WHERE files.file_id IS NOT NULL
|
|
1358
|
+
`;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
private async getCourseDeletionImpact(courseId: number) {
|
|
749
1362
|
const rows = await this.prisma.$queryRaw<
|
|
750
|
-
Array<{
|
|
1363
|
+
Array<{
|
|
1364
|
+
file_id: number | null;
|
|
1365
|
+
file_size: number | null;
|
|
1366
|
+
source_type: string;
|
|
1367
|
+
}>
|
|
751
1368
|
>`
|
|
752
|
-
SELECT DISTINCT files.file_id
|
|
1369
|
+
SELECT DISTINCT files.file_id, files.file_size, files.source_type
|
|
753
1370
|
FROM (
|
|
754
|
-
SELECT ci.file_id
|
|
1371
|
+
SELECT ci.file_id, f.size AS file_size, 'course_image' AS source_type
|
|
755
1372
|
FROM course_image ci
|
|
1373
|
+
INNER JOIN file f ON f.id = ci.file_id
|
|
756
1374
|
WHERE ci.course_id = ${courseId}
|
|
757
1375
|
|
|
758
1376
|
UNION ALL
|
|
759
1377
|
|
|
760
|
-
SELECT cf.file_id
|
|
1378
|
+
SELECT cf.file_id, f.size AS file_size, 'course_file' AS source_type
|
|
761
1379
|
FROM course_file cf
|
|
1380
|
+
INNER JOIN file f ON f.id = cf.file_id
|
|
762
1381
|
WHERE cf.course_id = ${courseId}
|
|
763
1382
|
|
|
764
1383
|
UNION ALL
|
|
765
1384
|
|
|
766
|
-
SELECT clf.file_id
|
|
1385
|
+
SELECT clf.file_id, f.size AS file_size, 'course_lesson_file' AS source_type
|
|
767
1386
|
FROM course_lesson_file clf
|
|
1387
|
+
INNER JOIN file f ON f.id = clf.file_id
|
|
768
1388
|
INNER JOIN course_lesson cl ON cl.id = clf.course_lesson_id
|
|
769
1389
|
INNER JOIN course_module cm ON cm.id = cl.course_module_id
|
|
770
1390
|
WHERE cm.course_id = ${courseId}
|
|
771
1391
|
|
|
772
1392
|
UNION ALL
|
|
773
1393
|
|
|
774
|
-
SELECT clvf.file_id
|
|
1394
|
+
SELECT clvf.file_id, f.size AS file_size, 'course_lesson_video_frame' AS source_type
|
|
775
1395
|
FROM course_lesson_video_frame clvf
|
|
1396
|
+
INNER JOIN file f ON f.id = clvf.file_id
|
|
776
1397
|
INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
|
|
777
1398
|
INNER JOIN course_module cm ON cm.id = cl.course_module_id
|
|
778
1399
|
WHERE cm.course_id = ${courseId}
|
|
779
1400
|
|
|
780
1401
|
UNION ALL
|
|
781
1402
|
|
|
782
|
-
SELECT cgm.file_id
|
|
1403
|
+
SELECT cgm.file_id, f.size AS file_size, 'course_class_group_material' AS source_type
|
|
783
1404
|
FROM course_class_group_material cgm
|
|
1405
|
+
INNER JOIN file f ON f.id = cgm.file_id
|
|
784
1406
|
INNER JOIN course_class_group ccg ON ccg.id = cgm.course_class_group_id
|
|
785
1407
|
WHERE ccg.course_id = ${courseId}
|
|
1408
|
+
|
|
1409
|
+
UNION ALL
|
|
1410
|
+
|
|
1411
|
+
SELECT cln.frame_file_id AS file_id, f.size AS file_size, 'course_lesson_note_frame' AS source_type
|
|
1412
|
+
FROM course_lesson_note cln
|
|
1413
|
+
INNER JOIN file f ON f.id = cln.frame_file_id
|
|
1414
|
+
INNER JOIN course_lesson cl ON cl.id = cln.course_lesson_id
|
|
1415
|
+
INNER JOIN course_module cm ON cm.id = cl.course_module_id
|
|
1416
|
+
WHERE cm.course_id = ${courseId}
|
|
786
1417
|
) AS files
|
|
787
1418
|
WHERE files.file_id IS NOT NULL
|
|
788
1419
|
`;
|
|
789
1420
|
|
|
790
|
-
|
|
791
|
-
.
|
|
792
|
-
|
|
1421
|
+
const uniqueRows = Array.from(
|
|
1422
|
+
rows.reduce((map, row) => {
|
|
1423
|
+
const fileId = Number(row.file_id ?? 0);
|
|
1424
|
+
if (fileId > 0 && !map.has(fileId)) {
|
|
1425
|
+
map.set(fileId, {
|
|
1426
|
+
fileId,
|
|
1427
|
+
size: Number(row.file_size ?? 0) || 0,
|
|
1428
|
+
sourceType: row.source_type,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return map;
|
|
1432
|
+
}, new Map<number, { fileId: number; size: number; sourceType: string }>()).values(),
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
const countsBySource = rows.reduce<Record<string, number>>((acc, row) => {
|
|
1436
|
+
const key = String(row.source_type || 'unknown');
|
|
1437
|
+
acc[key] = (acc[key] ?? 0) + 1;
|
|
1438
|
+
return acc;
|
|
1439
|
+
}, {});
|
|
1440
|
+
|
|
1441
|
+
return {
|
|
1442
|
+
deletionImpact: {
|
|
1443
|
+
fileIds: uniqueRows.map((row) => row.fileId),
|
|
1444
|
+
fileCount: uniqueRows.length,
|
|
1445
|
+
totalBytes: uniqueRows.reduce((sum, row) => sum + row.size, 0),
|
|
1446
|
+
countsBySource,
|
|
1447
|
+
},
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private formatBytes(bytes: number) {
|
|
1452
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
1453
|
+
return '0 B';
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1457
|
+
let value = bytes;
|
|
1458
|
+
let unitIndex = 0;
|
|
1459
|
+
|
|
1460
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
1461
|
+
value /= 1024;
|
|
1462
|
+
unitIndex += 1;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
|
|
1466
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private async getCourseRelatedFileIds(courseId: number) {
|
|
1470
|
+
const impact = await this.getCourseDeletionImpact(courseId);
|
|
1471
|
+
|
|
1472
|
+
return impact.deletionImpact.fileIds.filter(
|
|
1473
|
+
(id) => Number.isInteger(id) && id > 0,
|
|
1474
|
+
);
|
|
793
1475
|
}
|
|
794
1476
|
|
|
795
1477
|
private mapCourse(
|
|
@@ -819,6 +1501,7 @@ export class CourseService {
|
|
|
819
1501
|
} | null;
|
|
820
1502
|
} | null;
|
|
821
1503
|
}>,
|
|
1504
|
+
hasRunningJobs = false,
|
|
822
1505
|
) {
|
|
823
1506
|
const resolvedName = extras?.name ?? c.name ?? null;
|
|
824
1507
|
const resolvedTitle = this.normalizeOptionalText(c.title) ?? c.slug;
|
|
@@ -905,6 +1588,7 @@ export class CourseService {
|
|
|
905
1588
|
(c.certificate_template_id
|
|
906
1589
|
? String(c.certificate_template_id)
|
|
907
1590
|
: null),
|
|
1591
|
+
hasRunningJobs,
|
|
908
1592
|
progressByModule: metrics?.progressByModule ?? [],
|
|
909
1593
|
instructors: Array.from(
|
|
910
1594
|
new Map(
|
|
@@ -980,6 +1664,25 @@ export class CourseService {
|
|
|
980
1664
|
return extrasById;
|
|
981
1665
|
}
|
|
982
1666
|
|
|
1667
|
+
private async getCoursesWithRunningJobs(
|
|
1668
|
+
courseIds: number[],
|
|
1669
|
+
): Promise<Set<number>> {
|
|
1670
|
+
if (courseIds.length === 0) return new Set();
|
|
1671
|
+
|
|
1672
|
+
const rows = (await this.prisma.$queryRawUnsafe(`
|
|
1673
|
+
SELECT DISTINCT cm.course_id
|
|
1674
|
+
FROM queue_job qj
|
|
1675
|
+
INNER JOIN course_lesson cl ON cl.id = CAST(qj.source_entity_id AS INTEGER)
|
|
1676
|
+
INNER JOIN course_module cm ON cm.id = cl.course_module_id
|
|
1677
|
+
WHERE qj.source_entity = 'course_lesson'
|
|
1678
|
+
AND qj.source_module = 'lms'
|
|
1679
|
+
AND qj.status IN ('pending', 'scheduled', 'processing', 'retrying')
|
|
1680
|
+
AND cm.course_id IN (${courseIds.join(',')})
|
|
1681
|
+
`)) as Array<{ course_id: number }>;
|
|
1682
|
+
|
|
1683
|
+
return new Set(rows.map((r) => Number(r.course_id)));
|
|
1684
|
+
}
|
|
1685
|
+
|
|
983
1686
|
private async persistCourseExtras(
|
|
984
1687
|
id: number,
|
|
985
1688
|
data: PersistCourseExtrasInput,
|
|
@@ -1009,7 +1712,8 @@ export class CourseService {
|
|
|
1009
1712
|
}
|
|
1010
1713
|
|
|
1011
1714
|
if (data.offeringType !== undefined) {
|
|
1012
|
-
|
|
1715
|
+
values.push(data.offeringType);
|
|
1716
|
+
setClauses.push(`offering_type = $${values.length}::course_offering_type_8af4d2739d_enum`);
|
|
1013
1717
|
}
|
|
1014
1718
|
|
|
1015
1719
|
if (setClauses.length === 0) {
|
|
@@ -1026,8 +1730,8 @@ export class CourseService {
|
|
|
1026
1730
|
`,
|
|
1027
1731
|
...values,
|
|
1028
1732
|
);
|
|
1029
|
-
} catch {
|
|
1030
|
-
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
console.error('[persistCourseExtras] failed:', err);
|
|
1031
1735
|
}
|
|
1032
1736
|
}
|
|
1033
1737
|
|
|
@@ -1192,50 +1896,176 @@ export class CourseService {
|
|
|
1192
1896
|
});
|
|
1193
1897
|
}
|
|
1194
1898
|
|
|
1195
|
-
async
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1899
|
+
async getCourseContentOverview(courseId: number) {
|
|
1900
|
+
const [
|
|
1901
|
+
moduleCount,
|
|
1902
|
+
lessons,
|
|
1903
|
+
transcriptionRows,
|
|
1904
|
+
xpMapRows,
|
|
1905
|
+
extractedImageCount,
|
|
1906
|
+
resourceFileCount,
|
|
1907
|
+
storageRows,
|
|
1908
|
+
] = await Promise.all([
|
|
1909
|
+
this.prisma.course_module.count({ where: { course_id: courseId } }),
|
|
1910
|
+
this.prisma.course_lesson.findMany({
|
|
1911
|
+
where: { course_module: { course_id: courseId } },
|
|
1912
|
+
select: { id: true, type: true, content: true, published: true },
|
|
1913
|
+
}),
|
|
1914
|
+
this.prisma.course_lesson_transcription_segment.findMany({
|
|
1915
|
+
where: { course_lesson: { course_module: { course_id: courseId } } },
|
|
1916
|
+
select: { course_lesson_id: true },
|
|
1917
|
+
distinct: ['course_lesson_id'],
|
|
1918
|
+
}),
|
|
1919
|
+
this.prisma.lesson_xp_map.findMany({
|
|
1920
|
+
where: { course_lesson: { course_module: { course_id: courseId } } },
|
|
1921
|
+
select: { course_lesson_id: true },
|
|
1922
|
+
}),
|
|
1923
|
+
this.prisma.course_lesson_video_frame.count({
|
|
1924
|
+
where: { course_lesson: { course_module: { course_id: courseId } } },
|
|
1925
|
+
}),
|
|
1926
|
+
this.prisma.course_lesson_file.count({
|
|
1927
|
+
where: {
|
|
1928
|
+
course_lesson: { course_module: { course_id: courseId } },
|
|
1929
|
+
type: { in: ['student_download', 'supplementary_material'] },
|
|
1930
|
+
},
|
|
1931
|
+
}),
|
|
1932
|
+
this.getCourseStorageRows(courseId),
|
|
1933
|
+
]);
|
|
1201
1934
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1935
|
+
const transcriptionLessonIds = new Set(
|
|
1936
|
+
transcriptionRows.map((r) => r.course_lesson_id),
|
|
1937
|
+
);
|
|
1938
|
+
const xpLessonIds = new Set(xpMapRows.map((r) => r.course_lesson_id));
|
|
1939
|
+
|
|
1940
|
+
const lessonsByType = { video: 0, questao: 0, post: 0 };
|
|
1941
|
+
let publishedLessonCount = 0;
|
|
1942
|
+
let videoWithTranscription = 0;
|
|
1943
|
+
let videoWithXp = 0;
|
|
1944
|
+
|
|
1945
|
+
const categoryOrder = [
|
|
1946
|
+
'video_original',
|
|
1947
|
+
'video_profile',
|
|
1948
|
+
'lesson_audio',
|
|
1949
|
+
'extracted_image',
|
|
1950
|
+
'student_download',
|
|
1951
|
+
'supplementary_material',
|
|
1952
|
+
'course_image',
|
|
1953
|
+
'course_file',
|
|
1954
|
+
'other_lesson_file',
|
|
1955
|
+
'course_export',
|
|
1956
|
+
];
|
|
1957
|
+
|
|
1958
|
+
const uniqueStorageFiles = new Map<number, number>();
|
|
1959
|
+
const uniqueStorageCategoryEntries = new Set<string>();
|
|
1960
|
+
const storageCategoryMap = new Map<
|
|
1961
|
+
string,
|
|
1962
|
+
{ key: string; fileCount: number; totalBytes: number }
|
|
1963
|
+
>();
|
|
1964
|
+
|
|
1965
|
+
for (const row of storageRows) {
|
|
1966
|
+
const fileId = Number(row.file_id ?? 0);
|
|
1967
|
+
if (fileId <= 0) continue;
|
|
1968
|
+
|
|
1969
|
+
const size = Number(row.file_size ?? 0) || 0;
|
|
1970
|
+
const category = String(row.storage_category || 'other_lesson_file');
|
|
1971
|
+
|
|
1972
|
+
if (!uniqueStorageFiles.has(fileId)) {
|
|
1973
|
+
uniqueStorageFiles.set(fileId, size);
|
|
1974
|
+
}
|
|
1209
1975
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1976
|
+
const categoryEntryKey = `${category}:${fileId}`;
|
|
1977
|
+
if (uniqueStorageCategoryEntries.has(categoryEntryKey)) {
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1212
1980
|
|
|
1213
|
-
|
|
1214
|
-
await tx.course_video_resolution_profile.deleteMany({
|
|
1215
|
-
where: { course_id: courseId },
|
|
1216
|
-
});
|
|
1981
|
+
uniqueStorageCategoryEntries.add(categoryEntryKey);
|
|
1217
1982
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
})),
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1983
|
+
const current = storageCategoryMap.get(category) ?? {
|
|
1984
|
+
key: category,
|
|
1985
|
+
fileCount: 0,
|
|
1986
|
+
totalBytes: 0,
|
|
1987
|
+
};
|
|
1227
1988
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1989
|
+
current.fileCount += 1;
|
|
1990
|
+
current.totalBytes += size;
|
|
1991
|
+
storageCategoryMap.set(category, current);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const storageCategories = Array.from(storageCategoryMap.values()).sort(
|
|
1995
|
+
(a, b) => {
|
|
1996
|
+
const orderA = categoryOrder.indexOf(a.key);
|
|
1997
|
+
const orderB = categoryOrder.indexOf(b.key);
|
|
1998
|
+
|
|
1999
|
+
if (orderA !== -1 || orderB !== -1) {
|
|
2000
|
+
return (orderA === -1 ? Number.MAX_SAFE_INTEGER : orderA) -
|
|
2001
|
+
(orderB === -1 ? Number.MAX_SAFE_INTEGER : orderB);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
return b.totalBytes - a.totalBytes;
|
|
2005
|
+
},
|
|
1239
2006
|
);
|
|
2007
|
+
|
|
2008
|
+
for (const lesson of lessons) {
|
|
2009
|
+
if (lesson.published) publishedLessonCount++;
|
|
2010
|
+
|
|
2011
|
+
let sourceType: string | undefined;
|
|
2012
|
+
try {
|
|
2013
|
+
const parsed = lesson.content
|
|
2014
|
+
? (JSON.parse(lesson.content as string) as Record<string, unknown>)
|
|
2015
|
+
: null;
|
|
2016
|
+
sourceType =
|
|
2017
|
+
typeof parsed?.sourceType === 'string'
|
|
2018
|
+
? parsed.sourceType
|
|
2019
|
+
: undefined;
|
|
2020
|
+
} catch {
|
|
2021
|
+
// ignore malformed content
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
let uiType: 'video' | 'questao' | 'post';
|
|
2025
|
+
if (
|
|
2026
|
+
sourceType === 'video' ||
|
|
2027
|
+
sourceType === 'questao' ||
|
|
2028
|
+
sourceType === 'post'
|
|
2029
|
+
) {
|
|
2030
|
+
uiType = sourceType;
|
|
2031
|
+
} else if (lesson.type === 'video') {
|
|
2032
|
+
uiType = 'video';
|
|
2033
|
+
} else if (lesson.type === 'quiz') {
|
|
2034
|
+
uiType = 'questao';
|
|
2035
|
+
} else {
|
|
2036
|
+
uiType = 'post';
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
lessonsByType[uiType]++;
|
|
2040
|
+
|
|
2041
|
+
if (uiType === 'video') {
|
|
2042
|
+
if (transcriptionLessonIds.has(lesson.id)) videoWithTranscription++;
|
|
2043
|
+
if (xpLessonIds.has(lesson.id)) videoWithXp++;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
return {
|
|
2048
|
+
structure: {
|
|
2049
|
+
moduleCount,
|
|
2050
|
+
lessonCount: lessons.length,
|
|
2051
|
+
publishedLessonCount,
|
|
2052
|
+
lessonsByType,
|
|
2053
|
+
},
|
|
2054
|
+
videos: {
|
|
2055
|
+
lessonCount: lessonsByType.video,
|
|
2056
|
+
withTranscription: videoWithTranscription,
|
|
2057
|
+
withXp: videoWithXp,
|
|
2058
|
+
},
|
|
2059
|
+
media: { extractedImageCount },
|
|
2060
|
+
resources: { fileCount: resourceFileCount },
|
|
2061
|
+
storage: {
|
|
2062
|
+
totalBytes: Array.from(uniqueStorageFiles.values()).reduce(
|
|
2063
|
+
(sum, size) => sum + size,
|
|
2064
|
+
0,
|
|
2065
|
+
),
|
|
2066
|
+
totalFileCount: uniqueStorageFiles.size,
|
|
2067
|
+
categories: storageCategories,
|
|
2068
|
+
},
|
|
2069
|
+
};
|
|
1240
2070
|
}
|
|
1241
2071
|
}
|