@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
|
@@ -6,12 +6,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
6
6
|
import {
|
|
7
7
|
AlertTriangle,
|
|
8
8
|
BookOpen,
|
|
9
|
+
Briefcase,
|
|
9
10
|
CheckCircle2,
|
|
10
11
|
CircleDot,
|
|
11
12
|
Clock,
|
|
12
13
|
Download,
|
|
13
14
|
Layers,
|
|
14
15
|
Loader2,
|
|
16
|
+
Mic,
|
|
15
17
|
Pencil,
|
|
16
18
|
Plus,
|
|
17
19
|
Save,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
UploadCloud,
|
|
20
22
|
Video,
|
|
21
23
|
X,
|
|
24
|
+
Zap,
|
|
22
25
|
} from 'lucide-react';
|
|
23
26
|
import { useTranslations } from 'next-intl';
|
|
24
27
|
import { useRouter } from 'next/navigation';
|
|
@@ -28,12 +31,21 @@ import { toast } from 'sonner';
|
|
|
28
31
|
import { z } from 'zod';
|
|
29
32
|
|
|
30
33
|
import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
|
|
31
|
-
import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
|
|
32
34
|
import { FileTypeIcon } from '@/components/file-type-icon';
|
|
33
35
|
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
36
|
+
import {
|
|
37
|
+
AlertDialog,
|
|
38
|
+
AlertDialogAction,
|
|
39
|
+
AlertDialogCancel,
|
|
40
|
+
AlertDialogContent,
|
|
41
|
+
AlertDialogDescription,
|
|
42
|
+
AlertDialogFooter,
|
|
43
|
+
AlertDialogHeader,
|
|
44
|
+
AlertDialogTitle,
|
|
45
|
+
} from '@/components/ui/alert-dialog';
|
|
34
46
|
import { Badge } from '@/components/ui/badge';
|
|
35
47
|
import { Button } from '@/components/ui/button';
|
|
36
|
-
import {
|
|
48
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
37
49
|
import {
|
|
38
50
|
Dialog,
|
|
39
51
|
DialogContent,
|
|
@@ -42,6 +54,15 @@ import {
|
|
|
42
54
|
DialogHeader,
|
|
43
55
|
DialogTitle,
|
|
44
56
|
} from '@/components/ui/dialog';
|
|
57
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
58
|
+
import {
|
|
59
|
+
DropdownMenu,
|
|
60
|
+
DropdownMenuContent,
|
|
61
|
+
DropdownMenuItem,
|
|
62
|
+
DropdownMenuLabel,
|
|
63
|
+
DropdownMenuSeparator,
|
|
64
|
+
DropdownMenuTrigger,
|
|
65
|
+
} from '@/components/ui/dropdown-menu';
|
|
45
66
|
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
46
67
|
import {
|
|
47
68
|
Form,
|
|
@@ -74,6 +95,7 @@ import { cn } from '@/lib/utils';
|
|
|
74
95
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
75
96
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
76
97
|
|
|
98
|
+
import { CourseDeleteDialog } from '../../../_components/CourseDeleteDialog';
|
|
77
99
|
import type {
|
|
78
100
|
CourseEditFormValues,
|
|
79
101
|
PickerOption,
|
|
@@ -83,7 +105,6 @@ import { CourseClassificationCard } from '../../_components/CourseClassification
|
|
|
83
105
|
import { CourseContentCard } from '../../_components/CourseContentCard';
|
|
84
106
|
import { CourseDangerZoneCard } from '../../_components/CourseDangerZoneCard';
|
|
85
107
|
import { CourseFlagsCard } from '../../_components/CourseFlagsCard';
|
|
86
|
-
import { CourseInstructorsSummaryCard } from './CourseInstructorsSummaryCard';
|
|
87
108
|
import { CourseMediaCard } from '../../_components/CourseMediaCard';
|
|
88
109
|
import { CourseRelationsCard } from '../../_components/CourseRelationsCard';
|
|
89
110
|
import {
|
|
@@ -94,8 +115,14 @@ import {
|
|
|
94
115
|
} from '../_data/services/course-structure.service';
|
|
95
116
|
import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
96
117
|
import { courseStructureQueryKey } from '../_data/use-course-structure-query';
|
|
97
|
-
import {
|
|
118
|
+
import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
|
|
119
|
+
import { CourseExportSheet } from './course-export-sheet';
|
|
120
|
+
import { CourseExportsTab } from './course-exports-tab';
|
|
98
121
|
import { CourseOperationsTab } from './course-operations-tab';
|
|
122
|
+
import { CourseOverviewTab } from './course-overview-tab';
|
|
123
|
+
import { CourseXpOverviewTab } from './course-xp-overview-tab';
|
|
124
|
+
import { CourseInstructorsSummaryCard } from './CourseInstructorsSummaryCard';
|
|
125
|
+
import { IconActionTooltip } from './icon-action-tooltip';
|
|
99
126
|
import { useStructureStore } from './store';
|
|
100
127
|
import { Resource } from './types';
|
|
101
128
|
|
|
@@ -129,6 +156,8 @@ type ApiCourseDetail = {
|
|
|
129
156
|
instructors?: Array<{ id: number; name: string; avatarId: number | null }>;
|
|
130
157
|
logoFileId?: number | null;
|
|
131
158
|
logoFilename?: string | null;
|
|
159
|
+
logoFileSize?: number | null;
|
|
160
|
+
logoMimeType?: string | null;
|
|
132
161
|
logoImageType?: {
|
|
133
162
|
suggestedWidth: number | null;
|
|
134
163
|
suggestedHeight: number | null;
|
|
@@ -137,6 +166,8 @@ type ApiCourseDetail = {
|
|
|
137
166
|
} | null;
|
|
138
167
|
bannerFileId?: number | null;
|
|
139
168
|
bannerFilename?: string | null;
|
|
169
|
+
bannerFileSize?: number | null;
|
|
170
|
+
bannerMimeType?: string | null;
|
|
140
171
|
bannerImageType?: {
|
|
141
172
|
suggestedWidth: number | null;
|
|
142
173
|
suggestedHeight: number | null;
|
|
@@ -147,13 +178,26 @@ type ApiCourseDetail = {
|
|
|
147
178
|
operationsProjectId?: number | null;
|
|
148
179
|
operationsProjectCode?: string | null;
|
|
149
180
|
operationsProjectName?: string | null;
|
|
181
|
+
deletionImpact?: ApiCourseDeletionImpact;
|
|
150
182
|
};
|
|
151
183
|
|
|
152
|
-
type
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
type ApiCourseDeletionImpact = {
|
|
185
|
+
fileCount: number;
|
|
186
|
+
totalBytes: number;
|
|
187
|
+
formattedSize: string;
|
|
188
|
+
countsBySource?: Record<string, number>;
|
|
189
|
+
enrollmentCount?: number;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
type ApiQueuedDeleteResponse = {
|
|
193
|
+
success: boolean;
|
|
194
|
+
status: 'queued';
|
|
195
|
+
queueJobId: number;
|
|
196
|
+
notificationId: number;
|
|
197
|
+
deletionImpact?: Pick<
|
|
198
|
+
ApiCourseDeletionImpact,
|
|
199
|
+
'fileCount' | 'totalBytes' | 'formattedSize'
|
|
200
|
+
>;
|
|
157
201
|
};
|
|
158
202
|
|
|
159
203
|
type ApiCategory = { id: number; slug: string; name: string };
|
|
@@ -316,6 +360,24 @@ function formatFileSize(bytes: number): string {
|
|
|
316
360
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
317
361
|
}
|
|
318
362
|
|
|
363
|
+
function formatBytes(bytes: number) {
|
|
364
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
365
|
+
return '0 B';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
369
|
+
let value = bytes;
|
|
370
|
+
let unitIndex = 0;
|
|
371
|
+
|
|
372
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
373
|
+
value /= 1024;
|
|
374
|
+
unitIndex += 1;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
|
|
378
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
319
381
|
function mapApiCourseResourceToLocal(
|
|
320
382
|
item: CourseResourceApi
|
|
321
383
|
): CourseResourceItem {
|
|
@@ -378,9 +440,86 @@ function buildSchema(t: (key: string) => string) {
|
|
|
378
440
|
});
|
|
379
441
|
}
|
|
380
442
|
|
|
443
|
+
// ── Skeleton ──────────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
function EditorCourseSkeleton() {
|
|
446
|
+
return (
|
|
447
|
+
<div className="flex flex-col h-full min-h-0">
|
|
448
|
+
{/* Header */}
|
|
449
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
|
|
450
|
+
<Skeleton className="size-9 rounded-lg shrink-0" />
|
|
451
|
+
<div className="flex-1 min-w-0 flex flex-col gap-1">
|
|
452
|
+
<Skeleton className="h-4 w-16" />
|
|
453
|
+
<Skeleton className="h-3 w-32" />
|
|
454
|
+
</div>
|
|
455
|
+
<div className="flex items-center gap-1.5">
|
|
456
|
+
<Skeleton className="h-5 w-12 rounded-full" />
|
|
457
|
+
<Skeleton className="h-5 w-12 rounded-full" />
|
|
458
|
+
<Skeleton className="h-5 w-14 rounded-full" />
|
|
459
|
+
<Skeleton className="h-5 w-16 rounded-full" />
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
{/* Tabs bar */}
|
|
463
|
+
<div className="flex items-center gap-1 px-3 py-1 border-b bg-muted/50 shrink-0 overflow-hidden">
|
|
464
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
465
|
+
<Skeleton key={i} className="h-7 w-16 rounded-md shrink-0" />
|
|
466
|
+
))}
|
|
467
|
+
</div>
|
|
468
|
+
{/* Tab content — estrutura */}
|
|
469
|
+
<div className="flex-1 min-h-0 overflow-y-auto p-3 flex flex-col gap-3">
|
|
470
|
+
<div className="rounded-lg border bg-muted/20 p-3 flex flex-col gap-3">
|
|
471
|
+
<Skeleton className="h-3 w-28" />
|
|
472
|
+
{[80, 56, 96].map((w, i) => (
|
|
473
|
+
<div key={i} className="flex flex-col gap-1">
|
|
474
|
+
<Skeleton className="h-3 w-20" />
|
|
475
|
+
<Skeleton className={`h-8 w-full max-w-[${w}%]`} />
|
|
476
|
+
</div>
|
|
477
|
+
))}
|
|
478
|
+
</div>
|
|
479
|
+
<div className="rounded-lg border bg-muted/20 p-3 flex flex-col gap-3">
|
|
480
|
+
<Skeleton className="h-3 w-24" />
|
|
481
|
+
<div className="flex flex-col gap-1">
|
|
482
|
+
<Skeleton className="h-3 w-16" />
|
|
483
|
+
<Skeleton className="h-20 w-full" />
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
<div className="rounded-lg border bg-muted/20 p-3 flex flex-col gap-3">
|
|
487
|
+
<Skeleton className="h-3 w-20" />
|
|
488
|
+
<div className="grid grid-cols-2 gap-2">
|
|
489
|
+
{[1, 2, 3, 4].map((i) => (
|
|
490
|
+
<div key={i} className="flex flex-col gap-1">
|
|
491
|
+
<Skeleton className="h-3 w-14" />
|
|
492
|
+
<Skeleton className="h-8 w-full" />
|
|
493
|
+
</div>
|
|
494
|
+
))}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
381
502
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
382
503
|
|
|
383
|
-
|
|
504
|
+
const COURSE_TABS = [
|
|
505
|
+
'visao-geral',
|
|
506
|
+
'detalhes',
|
|
507
|
+
'xp',
|
|
508
|
+
'midia',
|
|
509
|
+
'recursos',
|
|
510
|
+
'operacoes',
|
|
511
|
+
'publicacao',
|
|
512
|
+
'extra',
|
|
513
|
+
'exportacoes',
|
|
514
|
+
] as const;
|
|
515
|
+
type CourseTab = (typeof COURSE_TABS)[number];
|
|
516
|
+
|
|
517
|
+
interface EditorCourseProps {
|
|
518
|
+
defaultTab?: string;
|
|
519
|
+
onTabChange?: (tab: string) => void;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function EditorCourse({ defaultTab, onTabChange }: EditorCourseProps) {
|
|
384
523
|
const courseId = useStructureStore((s) => s.courseId);
|
|
385
524
|
const course = useStructureStore((s) => s.course);
|
|
386
525
|
const sessions = useStructureStore((s) => s.sessions);
|
|
@@ -389,6 +528,7 @@ export function EditorCourse() {
|
|
|
389
528
|
|
|
390
529
|
const { request, currentLocaleCode, locales } = useApp();
|
|
391
530
|
const t = useTranslations('lms.CursoEditPage');
|
|
531
|
+
const courseListT = useTranslations('lms.CoursesPage');
|
|
392
532
|
const courseLocaleLabel = currentLocaleCode?.startsWith('en')
|
|
393
533
|
? 'Course Language'
|
|
394
534
|
: 'Idioma do Curso';
|
|
@@ -396,9 +536,24 @@ export function EditorCourse() {
|
|
|
396
536
|
const isMobile = useIsMobile();
|
|
397
537
|
const queryClient = useQueryClient();
|
|
398
538
|
const createSessionMutation = useCreateSessionMutation();
|
|
539
|
+
const lmsSettings = useLmsSettingsQuery();
|
|
399
540
|
|
|
400
541
|
// ── UI state ────────────────────────────────────────────────────────────────
|
|
401
|
-
const [activeTab, setActiveTab] = useState(
|
|
542
|
+
const [activeTab, setActiveTab] = useState<CourseTab>(() =>
|
|
543
|
+
defaultTab && (COURSE_TABS as readonly string[]).includes(defaultTab)
|
|
544
|
+
? (defaultTab as CourseTab)
|
|
545
|
+
: 'visao-geral'
|
|
546
|
+
);
|
|
547
|
+
const [exportSheetOpen, setExportSheetOpen] = useState(false);
|
|
548
|
+
const [pendingBulkJob, setPendingBulkJob] = useState<'transcription' | 'xp_recalculation' | 'video_processing' | null>(null);
|
|
549
|
+
const [isBulkJobPending, setIsBulkJobPending] = useState(false);
|
|
550
|
+
const [videoProcessingStats, setVideoProcessingStats] = useState<{
|
|
551
|
+
withOriginal: number;
|
|
552
|
+
withoutOriginal: number;
|
|
553
|
+
alreadyProcessed: number;
|
|
554
|
+
} | null>(null);
|
|
555
|
+
const [isFetchingVideoStats, setIsFetchingVideoStats] = useState(false);
|
|
556
|
+
const [reprocessAlreadyProcessed, setReprocessAlreadyProcessed] = useState(false);
|
|
402
557
|
const [categoryEditSheetOpen, setCategoryEditSheetOpen] = useState(false);
|
|
403
558
|
const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
|
|
404
559
|
null
|
|
@@ -417,7 +572,15 @@ export function EditorCourse() {
|
|
|
417
572
|
>('active');
|
|
418
573
|
const [savingCategoryEdit, setSavingCategoryEdit] = useState(false);
|
|
419
574
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
420
|
-
const [
|
|
575
|
+
const [confirmationInput, setConfirmationInput] = useState('');
|
|
576
|
+
const [archivingCourse, setArchivingCourse] = useState(false);
|
|
577
|
+
const [queuingDelete, setQueuingDelete] = useState(false);
|
|
578
|
+
const [loadingDeleteImpact, setLoadingDeleteImpact] = useState(false);
|
|
579
|
+
const [deleteImpact, setDeleteImpact] =
|
|
580
|
+
useState<ApiCourseDeletionImpact | null>(null);
|
|
581
|
+
const [deleteCourseStatus, setDeleteCourseStatus] = useState<
|
|
582
|
+
CourseEditFormValues['status'] | null
|
|
583
|
+
>(null);
|
|
421
584
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
|
422
585
|
const [bannerPreview, setBannerPreview] = useState<string | null>(null);
|
|
423
586
|
const [uploadingLogo, setUploadingLogo] = useState(false);
|
|
@@ -439,33 +602,23 @@ export function EditorCourse() {
|
|
|
439
602
|
const [isUploadingResources, setIsUploadingResources] = useState(false);
|
|
440
603
|
const [isSavingResources, setIsSavingResources] = useState(false);
|
|
441
604
|
const resourcesInputRef = useRef<HTMLInputElement>(null);
|
|
442
|
-
const [linkedProfileIds, setLinkedProfileIds] = useState<number[]>([]);
|
|
443
|
-
const [videoProfilePickerResetKey, setVideoProfilePickerResetKey] =
|
|
444
|
-
useState(0);
|
|
445
|
-
const [videoProfileEditSheetOpen, setVideoProfileEditSheetOpen] =
|
|
446
|
-
useState(false);
|
|
447
|
-
const [editingVideoProfileId, setEditingVideoProfileId] = useState<
|
|
448
|
-
number | null
|
|
449
|
-
>(null);
|
|
450
|
-
const [editingVideoProfileName, setEditingVideoProfileName] = useState('');
|
|
451
|
-
const [editingVideoProfileParams, setEditingVideoProfileParams] =
|
|
452
|
-
useState('');
|
|
453
|
-
const [savingVideoProfileEdit, setSavingVideoProfileEdit] = useState(false);
|
|
454
605
|
|
|
455
606
|
// ── Queries ─────────────────────────────────────────────────────────────────
|
|
456
|
-
const {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
607
|
+
const {
|
|
608
|
+
data: apiCourse,
|
|
609
|
+
refetch: refetchCourse,
|
|
610
|
+
isLoading: isLoadingCourse,
|
|
611
|
+
} = useQuery<ApiCourseDetail>({
|
|
612
|
+
queryKey: ['lms-course-detail', courseId],
|
|
613
|
+
enabled: Boolean(courseId),
|
|
614
|
+
queryFn: async () => {
|
|
615
|
+
const response = await request<ApiCourseDetail>({
|
|
616
|
+
url: `/lms/courses/${courseId}`,
|
|
617
|
+
method: 'GET',
|
|
618
|
+
});
|
|
619
|
+
return response.data;
|
|
620
|
+
},
|
|
621
|
+
});
|
|
469
622
|
|
|
470
623
|
const { data: categoryListData, refetch: refetchCategoryOptions } =
|
|
471
624
|
useQuery<ApiCategoryList>({
|
|
@@ -536,22 +689,6 @@ export function EditorCourse() {
|
|
|
536
689
|
initialData: { data: [] },
|
|
537
690
|
});
|
|
538
691
|
|
|
539
|
-
const {
|
|
540
|
-
data: allVideoProfiles = [],
|
|
541
|
-
refetch: refetchVideoProfiles,
|
|
542
|
-
isFetching: isFetchingVideoProfiles,
|
|
543
|
-
} = useQuery<VideoProfileOption[]>({
|
|
544
|
-
queryKey: ['lms-video-resolution-profiles-all'],
|
|
545
|
-
queryFn: async () => {
|
|
546
|
-
const response = await request<VideoProfileOption[]>({
|
|
547
|
-
url: '/lms/video-resolution-profiles/all',
|
|
548
|
-
method: 'GET',
|
|
549
|
-
});
|
|
550
|
-
return response.data;
|
|
551
|
-
},
|
|
552
|
-
initialData: [],
|
|
553
|
-
});
|
|
554
|
-
|
|
555
692
|
const { data: operationsConfig } = useQuery<{ isAvailable: boolean }>({
|
|
556
693
|
queryKey: ['lms-course-operations-config', courseId],
|
|
557
694
|
enabled: Boolean(courseId),
|
|
@@ -571,6 +708,12 @@ export function EditorCourse() {
|
|
|
571
708
|
|
|
572
709
|
const showOperationsTab = operationsConfig?.isAvailable === true;
|
|
573
710
|
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (apiCourse?.status) {
|
|
713
|
+
setDeleteCourseStatus(toPtStatus(apiCourse.status));
|
|
714
|
+
}
|
|
715
|
+
}, [apiCourse?.status]);
|
|
716
|
+
|
|
574
717
|
// ── Save mutation ───────────────────────────────────────────────────────────
|
|
575
718
|
const { mutate: saveCourse, isPending: saving } = useMutation({
|
|
576
719
|
mutationFn: async (data: CourseEditFormValues) => {
|
|
@@ -607,15 +750,6 @@ export function EditorCourse() {
|
|
|
607
750
|
return data;
|
|
608
751
|
},
|
|
609
752
|
onSuccess: (data) => {
|
|
610
|
-
if (courseId) {
|
|
611
|
-
void request({
|
|
612
|
-
url: `/lms/courses/${courseId}/video-resolution-profiles/sync`,
|
|
613
|
-
method: 'POST',
|
|
614
|
-
data: { profileIds: linkedProfileIds },
|
|
615
|
-
}).catch(() => {
|
|
616
|
-
toast.error('Erro ao sincronizar perfis de vídeo.');
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
753
|
setPersistedCertificateModel(data.modeloCertificado || '');
|
|
620
754
|
updateCourseInStore({
|
|
621
755
|
code: data.code,
|
|
@@ -759,14 +893,6 @@ export function EditorCourse() {
|
|
|
759
893
|
);
|
|
760
894
|
}, [certificateTemplateData, createdTemplateOptions]);
|
|
761
895
|
|
|
762
|
-
const availableVideoProfiles = useMemo(
|
|
763
|
-
() =>
|
|
764
|
-
allVideoProfiles.filter(
|
|
765
|
-
(profile) => !linkedProfileIds.includes(profile.id)
|
|
766
|
-
),
|
|
767
|
-
[allVideoProfiles, linkedProfileIds]
|
|
768
|
-
);
|
|
769
|
-
|
|
770
896
|
// ── Structural stats ─────────────────────────────────────────────────────────
|
|
771
897
|
const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
|
|
772
898
|
const hours = Math.floor(totalMinutes / 60);
|
|
@@ -777,9 +903,6 @@ export function EditorCourse() {
|
|
|
777
903
|
).length;
|
|
778
904
|
|
|
779
905
|
const formValues = form.watch();
|
|
780
|
-
const showVideoTab =
|
|
781
|
-
formValues.tipoOferta === 'sob_demanda' ||
|
|
782
|
-
formValues.tipoOferta === 'hibrido';
|
|
783
906
|
const descriptionText = String(formValues.descricaoPublica ?? '')
|
|
784
907
|
.replace(/<[^>]+>/g, '')
|
|
785
908
|
.trim();
|
|
@@ -911,32 +1034,6 @@ export function EditorCourse() {
|
|
|
911
1034
|
};
|
|
912
1035
|
}, [courseId, request]);
|
|
913
1036
|
|
|
914
|
-
useEffect(() => {
|
|
915
|
-
if (!showVideoTab && activeTab === 'videos') {
|
|
916
|
-
setActiveTab('estrutura');
|
|
917
|
-
}
|
|
918
|
-
}, [showVideoTab, activeTab]);
|
|
919
|
-
|
|
920
|
-
useEffect(() => {
|
|
921
|
-
if (!courseId) return;
|
|
922
|
-
let active = true;
|
|
923
|
-
void request<VideoProfileOption[]>({
|
|
924
|
-
url: `/lms/courses/${courseId}/video-resolution-profiles`,
|
|
925
|
-
method: 'GET',
|
|
926
|
-
})
|
|
927
|
-
.then((res) => {
|
|
928
|
-
if (!active) return;
|
|
929
|
-
setLinkedProfileIds((res.data ?? []).map((p) => p.id));
|
|
930
|
-
})
|
|
931
|
-
.catch(() => {
|
|
932
|
-
if (!active) return;
|
|
933
|
-
setLinkedProfileIds([]);
|
|
934
|
-
});
|
|
935
|
-
return () => {
|
|
936
|
-
active = false;
|
|
937
|
-
};
|
|
938
|
-
}, [courseId, request]);
|
|
939
|
-
|
|
940
1037
|
async function syncCourseResources(nextResources: CourseResourceItem[]) {
|
|
941
1038
|
setIsSavingResources(true);
|
|
942
1039
|
try {
|
|
@@ -1256,38 +1353,6 @@ export function EditorCourse() {
|
|
|
1256
1353
|
}
|
|
1257
1354
|
}
|
|
1258
1355
|
|
|
1259
|
-
function handleEditVideoProfile(profileId: number) {
|
|
1260
|
-
const profile = allVideoProfiles.find((p) => p.id === profileId);
|
|
1261
|
-
if (!profile) return;
|
|
1262
|
-
setEditingVideoProfileId(profileId);
|
|
1263
|
-
setEditingVideoProfileName(profile.name);
|
|
1264
|
-
setEditingVideoProfileParams(profile.ffmpeg_params);
|
|
1265
|
-
setVideoProfileEditSheetOpen(true);
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
async function handleSaveVideoProfileEdit() {
|
|
1269
|
-
if (!editingVideoProfileId) return;
|
|
1270
|
-
try {
|
|
1271
|
-
setSavingVideoProfileEdit(true);
|
|
1272
|
-
await request({
|
|
1273
|
-
url: `/lms/video-resolution-profiles/${editingVideoProfileId}`,
|
|
1274
|
-
method: 'PATCH',
|
|
1275
|
-
data: {
|
|
1276
|
-
name: editingVideoProfileName,
|
|
1277
|
-
ffmpeg_params: editingVideoProfileParams,
|
|
1278
|
-
},
|
|
1279
|
-
});
|
|
1280
|
-
await refetchVideoProfiles();
|
|
1281
|
-
setVideoProfileEditSheetOpen(false);
|
|
1282
|
-
setEditingVideoProfileId(null);
|
|
1283
|
-
toast.success('Perfil de vídeo atualizado com sucesso.');
|
|
1284
|
-
} catch {
|
|
1285
|
-
toast.error('Erro ao atualizar o perfil de vídeo.');
|
|
1286
|
-
} finally {
|
|
1287
|
-
setSavingVideoProfileEdit(false);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
1356
|
async function handleEditCategory(categorySlug: string) {
|
|
1292
1357
|
const category = (categoryListData?.data ?? []).find(
|
|
1293
1358
|
(item) => item.slug === categorySlug
|
|
@@ -1380,58 +1445,197 @@ export function EditorCourse() {
|
|
|
1380
1445
|
}
|
|
1381
1446
|
}
|
|
1382
1447
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1448
|
+
const deleteCourseTitle = apiCourse?.title?.trim() || course.title.trim();
|
|
1449
|
+
const deleteConfirmMatches = confirmationInput === deleteCourseTitle;
|
|
1450
|
+
const deleteRequiresArchive = deleteCourseStatus !== 'arquivado';
|
|
1451
|
+
const canDeleteCourse =
|
|
1452
|
+
Boolean(courseId) &&
|
|
1453
|
+
!deleteRequiresArchive &&
|
|
1454
|
+
deleteConfirmMatches &&
|
|
1455
|
+
!loadingDeleteImpact &&
|
|
1456
|
+
!queuingDelete &&
|
|
1457
|
+
!archivingCourse &&
|
|
1458
|
+
Boolean(deleteImpact);
|
|
1459
|
+
|
|
1460
|
+
async function loadDeleteImpact() {
|
|
1461
|
+
if (!courseId) return;
|
|
1462
|
+
|
|
1463
|
+
setLoadingDeleteImpact(true);
|
|
1464
|
+
setDeleteImpact(null);
|
|
1465
|
+
|
|
1385
1466
|
try {
|
|
1386
|
-
await request({
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1467
|
+
const response = await request<ApiCourseDetail>({
|
|
1468
|
+
url: `/lms/courses/${courseId}`,
|
|
1469
|
+
method: 'GET',
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const impact = response.data.deletionImpact;
|
|
1473
|
+
|
|
1474
|
+
setDeleteCourseStatus(toPtStatus(response.data.status));
|
|
1475
|
+
setDeleteImpact(
|
|
1476
|
+
impact
|
|
1477
|
+
? {
|
|
1478
|
+
...impact,
|
|
1479
|
+
formattedSize:
|
|
1480
|
+
impact.formattedSize || formatBytes(impact.totalBytes ?? 0),
|
|
1481
|
+
}
|
|
1482
|
+
: null
|
|
1483
|
+
);
|
|
1484
|
+
} catch {
|
|
1485
|
+
toast.error(courseListT('toasts.courseDeleteImpactLoadError'));
|
|
1390
1486
|
} finally {
|
|
1391
|
-
|
|
1487
|
+
setLoadingDeleteImpact(false);
|
|
1392
1488
|
}
|
|
1393
1489
|
}
|
|
1394
1490
|
|
|
1395
|
-
function
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
lesson.videoProvider !== 'file_storage'
|
|
1402
|
-
) {
|
|
1403
|
-
return false;
|
|
1404
|
-
}
|
|
1491
|
+
function openDeleteDialog() {
|
|
1492
|
+
setConfirmationInput('');
|
|
1493
|
+
setDeleteImpact(null);
|
|
1494
|
+
setDeleteDialogOpen(true);
|
|
1495
|
+
void loadDeleteImpact();
|
|
1496
|
+
}
|
|
1405
1497
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1498
|
+
async function archiveCourseForDelete() {
|
|
1499
|
+
if (!courseId || deleteCourseStatus === 'arquivado') return;
|
|
1409
1500
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1501
|
+
setArchivingCourse(true);
|
|
1502
|
+
try {
|
|
1503
|
+
await request({
|
|
1504
|
+
url: `/lms/courses/${courseId}`,
|
|
1505
|
+
method: 'PATCH',
|
|
1506
|
+
data: { status: 'archived' },
|
|
1414
1507
|
});
|
|
1415
1508
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1509
|
+
setDeleteCourseStatus('arquivado');
|
|
1510
|
+
form.setValue('status', 'arquivado', {
|
|
1511
|
+
shouldDirty: false,
|
|
1512
|
+
shouldTouch: false,
|
|
1513
|
+
shouldValidate: false,
|
|
1514
|
+
});
|
|
1515
|
+
toast.success(
|
|
1516
|
+
courseListT('toasts.courseArchived', { title: deleteCourseTitle })
|
|
1517
|
+
);
|
|
1518
|
+
await refetchCourse();
|
|
1519
|
+
await loadDeleteImpact();
|
|
1520
|
+
} catch {
|
|
1521
|
+
toast.error(courseListT('toasts.courseArchiveError'));
|
|
1522
|
+
} finally {
|
|
1523
|
+
setArchivingCourse(false);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
async function handleDelete() {
|
|
1528
|
+
if (!courseId) return;
|
|
1529
|
+
if (confirmationInput !== deleteCourseTitle) {
|
|
1530
|
+
toast.error(
|
|
1531
|
+
courseListT('form.validationErrors.confirmationMismatch') ||
|
|
1532
|
+
'Nome do curso não corresponde.'
|
|
1533
|
+
);
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
if (deleteCourseStatus !== 'arquivado') {
|
|
1538
|
+
toast.error(courseListT('toasts.deleteOnlyArchived'));
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (!deleteImpact) {
|
|
1543
|
+
toast.error(courseListT('toasts.courseDeleteImpactLoadError'));
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
setQueuingDelete(true);
|
|
1548
|
+
try {
|
|
1549
|
+
const response = await request<ApiQueuedDeleteResponse>({
|
|
1550
|
+
url: `/lms/courses/${courseId}`,
|
|
1551
|
+
method: 'DELETE',
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
setDeleteDialogOpen(false);
|
|
1555
|
+
toast.success(
|
|
1556
|
+
courseListT('toasts.courseDeleteQueued', {
|
|
1557
|
+
title: deleteCourseTitle,
|
|
1558
|
+
files:
|
|
1559
|
+
response.data?.deletionImpact?.fileCount ?? deleteImpact.fileCount,
|
|
1560
|
+
})
|
|
1561
|
+
);
|
|
1562
|
+
router.push('/lms/courses');
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
const message =
|
|
1565
|
+
error instanceof Error && error.message ? error.message : '';
|
|
1566
|
+
|
|
1567
|
+
if (message.includes('ONLY_ARCHIVED_COURSE_CAN_BE_DELETED')) {
|
|
1568
|
+
toast.error(courseListT('toasts.deleteOnlyArchived'));
|
|
1569
|
+
} else {
|
|
1570
|
+
toast.error(courseListT('toasts.courseDeleteQueueError'));
|
|
1423
1571
|
}
|
|
1572
|
+
} finally {
|
|
1573
|
+
setQueuingDelete(false);
|
|
1424
1574
|
}
|
|
1575
|
+
}
|
|
1425
1576
|
|
|
1577
|
+
function onSubmit(data: CourseEditFormValues) {
|
|
1426
1578
|
saveCourse(data);
|
|
1427
1579
|
}
|
|
1428
1580
|
|
|
1581
|
+
function handlePublish() {
|
|
1582
|
+
const values = form.getValues();
|
|
1583
|
+
saveCourse({ ...values, status: 'publicado' });
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1429
1586
|
function onFormError(errors: Record<string, unknown>) {
|
|
1430
1587
|
const first = Object.values(errors)[0] as { message?: string } | undefined;
|
|
1431
1588
|
toast.error(first?.message ?? 'Verifique os campos obrigatórios');
|
|
1432
1589
|
}
|
|
1433
1590
|
|
|
1591
|
+
// ── Bulk jobs ─────────────────────────────────────────────────────────────────
|
|
1592
|
+
const handleBulkJob = async (
|
|
1593
|
+
jobType: 'transcription' | 'xp_recalculation' | 'video_processing',
|
|
1594
|
+
opts?: { reprocessAlreadyProcessed?: boolean },
|
|
1595
|
+
) => {
|
|
1596
|
+
setIsBulkJobPending(true);
|
|
1597
|
+
try {
|
|
1598
|
+
const res = await request<{ queued: number; skipped: number }>({
|
|
1599
|
+
url: `/lms/courses/${courseId}/structure/bulk-jobs`,
|
|
1600
|
+
method: 'POST',
|
|
1601
|
+
data: { jobType, ...opts },
|
|
1602
|
+
});
|
|
1603
|
+
toast.success(`${res.data?.queued ?? 0} job(s) enfileirado(s), ${res.data?.skipped ?? 0} ignorado(s).`);
|
|
1604
|
+
} catch {
|
|
1605
|
+
toast.error('Erro ao enfileirar jobs em massa.');
|
|
1606
|
+
} finally {
|
|
1607
|
+
setIsBulkJobPending(false);
|
|
1608
|
+
setPendingBulkJob(null);
|
|
1609
|
+
setVideoProcessingStats(null);
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
const handleOpenVideoProcessingDialog = async () => {
|
|
1614
|
+
setIsFetchingVideoStats(true);
|
|
1615
|
+
setReprocessAlreadyProcessed(false);
|
|
1616
|
+
try {
|
|
1617
|
+
const res = await request<{
|
|
1618
|
+
withOriginal: number;
|
|
1619
|
+
withoutOriginal: number;
|
|
1620
|
+
alreadyProcessed: number;
|
|
1621
|
+
}>({
|
|
1622
|
+
url: `/lms/courses/${courseId}/structure/video-processing-stats`,
|
|
1623
|
+
method: 'GET',
|
|
1624
|
+
});
|
|
1625
|
+
setVideoProcessingStats(res.data ?? null);
|
|
1626
|
+
setPendingBulkJob('video_processing');
|
|
1627
|
+
} catch {
|
|
1628
|
+
toast.error('Erro ao buscar estatísticas de processamento de vídeo.');
|
|
1629
|
+
} finally {
|
|
1630
|
+
setIsFetchingVideoStats(false);
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1434
1634
|
// ── Render ────────────────────────────────────────────────────────────────────
|
|
1635
|
+
if (isLoadingCourse && !apiCourse) {
|
|
1636
|
+
return <EditorCourseSkeleton />;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1435
1639
|
return (
|
|
1436
1640
|
<Form {...form}>
|
|
1437
1641
|
<form
|
|
@@ -1553,13 +1757,157 @@ export function EditorCourse() {
|
|
|
1553
1757
|
</TooltipContent>
|
|
1554
1758
|
</Tooltip>
|
|
1555
1759
|
</TooltipProvider>
|
|
1760
|
+
|
|
1761
|
+
{/* ── Bulk jobs menu ────────────────────────────────────────── */}
|
|
1762
|
+
<DropdownMenu>
|
|
1763
|
+
<DropdownMenuTrigger asChild>
|
|
1764
|
+
<Button
|
|
1765
|
+
type="button"
|
|
1766
|
+
variant="ghost"
|
|
1767
|
+
size="icon"
|
|
1768
|
+
className="size-6 shrink-0"
|
|
1769
|
+
disabled={isBulkJobPending || isFetchingVideoStats}
|
|
1770
|
+
>
|
|
1771
|
+
{isBulkJobPending || isFetchingVideoStats ? (
|
|
1772
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
1773
|
+
) : (
|
|
1774
|
+
<Briefcase className="size-3.5" />
|
|
1775
|
+
)}
|
|
1776
|
+
</Button>
|
|
1777
|
+
</DropdownMenuTrigger>
|
|
1778
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
1779
|
+
<DropdownMenuLabel className="text-xs">Jobs em massa</DropdownMenuLabel>
|
|
1780
|
+
<DropdownMenuSeparator />
|
|
1781
|
+
{lmsSettings.transcriptionEnabled && (
|
|
1782
|
+
<DropdownMenuItem
|
|
1783
|
+
onSelect={() => setPendingBulkJob('transcription')}
|
|
1784
|
+
className="gap-2"
|
|
1785
|
+
>
|
|
1786
|
+
<Mic className="size-4 text-muted-foreground" />
|
|
1787
|
+
Refazer transcrição de todas as aulas
|
|
1788
|
+
</DropdownMenuItem>
|
|
1789
|
+
)}
|
|
1790
|
+
<DropdownMenuItem
|
|
1791
|
+
onSelect={() => setPendingBulkJob('xp_recalculation')}
|
|
1792
|
+
className="gap-2"
|
|
1793
|
+
>
|
|
1794
|
+
<Zap className="size-4 text-muted-foreground" />
|
|
1795
|
+
Recalcular XP de todas as aulas
|
|
1796
|
+
</DropdownMenuItem>
|
|
1797
|
+
<DropdownMenuItem
|
|
1798
|
+
onSelect={handleOpenVideoProcessingDialog}
|
|
1799
|
+
className="gap-2"
|
|
1800
|
+
>
|
|
1801
|
+
<Video className="size-4 text-muted-foreground" />
|
|
1802
|
+
Processar todos os vídeos
|
|
1803
|
+
</DropdownMenuItem>
|
|
1804
|
+
<DropdownMenuSeparator />
|
|
1805
|
+
<DropdownMenuLabel className="text-xs">Exportar</DropdownMenuLabel>
|
|
1806
|
+
<DropdownMenuItem
|
|
1807
|
+
onSelect={() => setExportSheetOpen(true)}
|
|
1808
|
+
className="gap-2"
|
|
1809
|
+
>
|
|
1810
|
+
<Download className="size-4 text-muted-foreground" />
|
|
1811
|
+
SCORM 1.2
|
|
1812
|
+
</DropdownMenuItem>
|
|
1813
|
+
</DropdownMenuContent>
|
|
1814
|
+
</DropdownMenu>
|
|
1815
|
+
|
|
1816
|
+
<AlertDialog
|
|
1817
|
+
open={pendingBulkJob === 'transcription' || pendingBulkJob === 'xp_recalculation'}
|
|
1818
|
+
onOpenChange={(open) => { if (!open) setPendingBulkJob(null); }}
|
|
1819
|
+
>
|
|
1820
|
+
<AlertDialogContent>
|
|
1821
|
+
<AlertDialogHeader>
|
|
1822
|
+
<AlertDialogTitle>
|
|
1823
|
+
{pendingBulkJob === 'transcription'
|
|
1824
|
+
? 'Refazer transcrição de todas as aulas?'
|
|
1825
|
+
: 'Recalcular XP de todas as aulas?'}
|
|
1826
|
+
</AlertDialogTitle>
|
|
1827
|
+
<AlertDialogDescription>
|
|
1828
|
+
{pendingBulkJob === 'transcription'
|
|
1829
|
+
? `Um job de transcrição será enfileirado para cada aula que possuir arquivo de áudio (${lessons.length} aulas no total). Aulas sem áudio serão ignoradas.`
|
|
1830
|
+
: `Um job de recálculo de XP será enfileirado para cada aula que possuir transcrição (${lessons.length} aulas no total). Aulas sem transcrição serão ignoradas.`}
|
|
1831
|
+
</AlertDialogDescription>
|
|
1832
|
+
</AlertDialogHeader>
|
|
1833
|
+
<AlertDialogFooter>
|
|
1834
|
+
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
1835
|
+
<AlertDialogAction
|
|
1836
|
+
onClick={() => pendingBulkJob && handleBulkJob(pendingBulkJob as 'transcription' | 'xp_recalculation')}
|
|
1837
|
+
>
|
|
1838
|
+
Confirmar
|
|
1839
|
+
</AlertDialogAction>
|
|
1840
|
+
</AlertDialogFooter>
|
|
1841
|
+
</AlertDialogContent>
|
|
1842
|
+
</AlertDialog>
|
|
1843
|
+
|
|
1844
|
+
<Dialog
|
|
1845
|
+
open={pendingBulkJob === 'video_processing'}
|
|
1846
|
+
onOpenChange={(open) => {
|
|
1847
|
+
if (!open) {
|
|
1848
|
+
setPendingBulkJob(null);
|
|
1849
|
+
setVideoProcessingStats(null);
|
|
1850
|
+
}
|
|
1851
|
+
}}
|
|
1852
|
+
>
|
|
1853
|
+
<DialogContent className="sm:max-w-md">
|
|
1854
|
+
<DialogHeader>
|
|
1855
|
+
<DialogTitle>Processar todos os vídeos?</DialogTitle>
|
|
1856
|
+
<DialogDescription>
|
|
1857
|
+
{videoProcessingStats ? (
|
|
1858
|
+
<>
|
|
1859
|
+
<span className="font-medium text-foreground">{videoProcessingStats.withOriginal}</span> aula(s) com vídeo original elegíveis para processamento.{' '}
|
|
1860
|
+
<span className="font-medium text-foreground">{videoProcessingStats.withoutOriginal}</span> aula(s) sem vídeo original serão ignoradas.
|
|
1861
|
+
</>
|
|
1862
|
+
) : (
|
|
1863
|
+
'Carregando estatísticas...'
|
|
1864
|
+
)}
|
|
1865
|
+
</DialogDescription>
|
|
1866
|
+
</DialogHeader>
|
|
1867
|
+
{videoProcessingStats && videoProcessingStats.alreadyProcessed > 0 && (
|
|
1868
|
+
<div className="flex items-center gap-2 py-1">
|
|
1869
|
+
<Checkbox
|
|
1870
|
+
id="reprocess-checkbox"
|
|
1871
|
+
checked={reprocessAlreadyProcessed}
|
|
1872
|
+
onCheckedChange={(checked) => setReprocessAlreadyProcessed(checked === true)}
|
|
1873
|
+
/>
|
|
1874
|
+
<label htmlFor="reprocess-checkbox" className="text-sm leading-none cursor-pointer">
|
|
1875
|
+
Reprocessar aulas que já foram processadas{' '}
|
|
1876
|
+
<span className="text-muted-foreground">({videoProcessingStats.alreadyProcessed} aula(s))</span>
|
|
1877
|
+
</label>
|
|
1878
|
+
</div>
|
|
1879
|
+
)}
|
|
1880
|
+
<DialogFooter>
|
|
1881
|
+
<Button
|
|
1882
|
+
variant="outline"
|
|
1883
|
+
onClick={() => {
|
|
1884
|
+
setPendingBulkJob(null);
|
|
1885
|
+
setVideoProcessingStats(null);
|
|
1886
|
+
}}
|
|
1887
|
+
>
|
|
1888
|
+
Cancelar
|
|
1889
|
+
</Button>
|
|
1890
|
+
<Button
|
|
1891
|
+
disabled={isBulkJobPending || !videoProcessingStats}
|
|
1892
|
+
onClick={() => handleBulkJob('video_processing', { reprocessAlreadyProcessed })}
|
|
1893
|
+
>
|
|
1894
|
+
{isBulkJobPending && <Loader2 className="size-4 animate-spin mr-2" />}
|
|
1895
|
+
Confirmar
|
|
1896
|
+
</Button>
|
|
1897
|
+
</DialogFooter>
|
|
1898
|
+
</DialogContent>
|
|
1899
|
+
</Dialog>
|
|
1556
1900
|
</div>
|
|
1557
1901
|
</div>
|
|
1558
1902
|
|
|
1559
1903
|
{/* ── Tabs ─────────────────────────────────────────────────────────── */}
|
|
1560
1904
|
<Tabs
|
|
1561
1905
|
value={activeTab}
|
|
1562
|
-
onValueChange={
|
|
1906
|
+
onValueChange={(v) => {
|
|
1907
|
+
const tab = v as CourseTab;
|
|
1908
|
+
setActiveTab(tab);
|
|
1909
|
+
onTabChange?.(tab);
|
|
1910
|
+
}}
|
|
1563
1911
|
className="flex flex-1 min-h-0 min-w-0 flex-col"
|
|
1564
1912
|
>
|
|
1565
1913
|
<TabsList
|
|
@@ -1573,14 +1921,15 @@ export function EditorCourse() {
|
|
|
1573
1921
|
>
|
|
1574
1922
|
{(
|
|
1575
1923
|
[
|
|
1576
|
-
'
|
|
1577
|
-
'
|
|
1924
|
+
'visao-geral',
|
|
1925
|
+
'detalhes',
|
|
1926
|
+
'xp',
|
|
1578
1927
|
'midia',
|
|
1579
1928
|
'recursos',
|
|
1580
|
-
'extra',
|
|
1581
|
-
...(showVideoTab ? ['videos'] : []),
|
|
1582
1929
|
...(showOperationsTab ? ['operacoes'] : []),
|
|
1583
1930
|
'publicacao',
|
|
1931
|
+
'extra',
|
|
1932
|
+
'exportacoes',
|
|
1584
1933
|
] as const
|
|
1585
1934
|
).map((tab) => (
|
|
1586
1935
|
<TabsTrigger
|
|
@@ -1593,30 +1942,40 @@ export function EditorCourse() {
|
|
|
1593
1942
|
: 'h-8 px-3 text-xs'
|
|
1594
1943
|
)}
|
|
1595
1944
|
>
|
|
1596
|
-
{tab === '
|
|
1597
|
-
? t('structureEditor.tabs.
|
|
1598
|
-
: tab === '
|
|
1599
|
-
? t('structureEditor.tabs.
|
|
1600
|
-
: tab === '
|
|
1601
|
-
? t('structureEditor.tabs.
|
|
1602
|
-
: tab === '
|
|
1603
|
-
? t('structureEditor.tabs.
|
|
1604
|
-
: tab === '
|
|
1605
|
-
? t('structureEditor.tabs.
|
|
1606
|
-
: tab === '
|
|
1607
|
-
? t('structureEditor.tabs.
|
|
1945
|
+
{tab === 'visao-geral'
|
|
1946
|
+
? t('structureEditor.tabs.overview')
|
|
1947
|
+
: tab === 'detalhes'
|
|
1948
|
+
? t('structureEditor.tabs.details')
|
|
1949
|
+
: tab === 'xp'
|
|
1950
|
+
? t('structureEditor.tabs.xp')
|
|
1951
|
+
: tab === 'midia'
|
|
1952
|
+
? t('structureEditor.tabs.media')
|
|
1953
|
+
: tab === 'recursos'
|
|
1954
|
+
? t('structureEditor.tabs.resources')
|
|
1955
|
+
: tab === 'extra'
|
|
1956
|
+
? t('structureEditor.tabs.extra')
|
|
1608
1957
|
: tab === 'operacoes'
|
|
1609
|
-
|
|
1610
|
-
|
|
1958
|
+
? 'Operações'
|
|
1959
|
+
: tab === 'exportacoes'
|
|
1960
|
+
? 'Exportações'
|
|
1961
|
+
: t('structureEditor.tabs.publish')}
|
|
1611
1962
|
</TabsTrigger>
|
|
1612
1963
|
))}
|
|
1613
1964
|
</TabsList>
|
|
1614
1965
|
|
|
1615
1966
|
<div className="flex-1 min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain">
|
|
1616
1967
|
<div className="flex min-w-0 flex-col gap-3 p-2 sm:p-3">
|
|
1617
|
-
{/* ── Tab:
|
|
1968
|
+
{/* ── Tab: Visão Geral ─────────────────────────────────────── */}
|
|
1969
|
+
<TabsContent value="visao-geral" className="mt-0 min-w-0">
|
|
1970
|
+
<CourseOverviewTab
|
|
1971
|
+
courseId={courseId}
|
|
1972
|
+
locale={currentLocaleCode || 'pt-BR'}
|
|
1973
|
+
/>
|
|
1974
|
+
</TabsContent>
|
|
1975
|
+
|
|
1976
|
+
{/* ── Tab: Detalhes ───────────────────────────────────────── */}
|
|
1618
1977
|
<TabsContent
|
|
1619
|
-
value="
|
|
1978
|
+
value="detalhes"
|
|
1620
1979
|
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1621
1980
|
>
|
|
1622
1981
|
{/* Dados principais */}
|
|
@@ -1828,13 +2187,6 @@ export function EditorCourse() {
|
|
|
1828
2187
|
/>
|
|
1829
2188
|
</CardContent>
|
|
1830
2189
|
</Card>
|
|
1831
|
-
</TabsContent>
|
|
1832
|
-
|
|
1833
|
-
{/* ── Tab: Sobre ──────────────────────────────────────────── */}
|
|
1834
|
-
<TabsContent
|
|
1835
|
-
value="sobre"
|
|
1836
|
-
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
1837
|
-
>
|
|
1838
2190
|
<CourseClassificationCard
|
|
1839
2191
|
form={form}
|
|
1840
2192
|
t={t}
|
|
@@ -1859,6 +2211,13 @@ export function EditorCourse() {
|
|
|
1859
2211
|
<CourseContentCard form={form} compact />
|
|
1860
2212
|
</TabsContent>
|
|
1861
2213
|
|
|
2214
|
+
<TabsContent value="xp" className="mt-0 min-w-0">
|
|
2215
|
+
<CourseXpOverviewTab
|
|
2216
|
+
courseId={courseId}
|
|
2217
|
+
locale={currentLocaleCode || 'pt-BR'}
|
|
2218
|
+
/>
|
|
2219
|
+
</TabsContent>
|
|
2220
|
+
|
|
1862
2221
|
{/* ── Tab: Mídia ──────────────────────────────────────────── */}
|
|
1863
2222
|
<TabsContent value="midia" className="mt-0 min-w-0">
|
|
1864
2223
|
<CourseMediaCard
|
|
@@ -1879,6 +2238,8 @@ export function EditorCourse() {
|
|
|
1879
2238
|
name:
|
|
1880
2239
|
apiCourse.logoFilename ||
|
|
1881
2240
|
`#${apiCourse.logoFileId}`,
|
|
2241
|
+
size: apiCourse.logoFileSize ?? null,
|
|
2242
|
+
mimeType: apiCourse.logoMimeType ?? null,
|
|
1882
2243
|
}
|
|
1883
2244
|
: undefined
|
|
1884
2245
|
}
|
|
@@ -1889,6 +2250,8 @@ export function EditorCourse() {
|
|
|
1889
2250
|
name:
|
|
1890
2251
|
apiCourse.bannerFilename ||
|
|
1891
2252
|
`#${apiCourse.bannerFileId}`,
|
|
2253
|
+
size: apiCourse.bannerFileSize ?? null,
|
|
2254
|
+
mimeType: apiCourse.bannerMimeType ?? null,
|
|
1892
2255
|
}
|
|
1893
2256
|
: undefined
|
|
1894
2257
|
}
|
|
@@ -2121,179 +2484,11 @@ export function EditorCourse() {
|
|
|
2121
2484
|
<CourseFlagsCard form={form} t={t} compact />
|
|
2122
2485
|
<CourseDangerZoneCard
|
|
2123
2486
|
t={t}
|
|
2124
|
-
onDelete={
|
|
2487
|
+
onDelete={openDeleteDialog}
|
|
2125
2488
|
compact
|
|
2126
2489
|
/>
|
|
2127
2490
|
</TabsContent>
|
|
2128
2491
|
|
|
2129
|
-
{/* ── Tab: Vídeos (on_demand / blended only) ─────────────── */}
|
|
2130
|
-
{showVideoTab && (
|
|
2131
|
-
<TabsContent
|
|
2132
|
-
value="videos"
|
|
2133
|
-
className="mt-0 flex min-w-0 flex-col gap-3"
|
|
2134
|
-
>
|
|
2135
|
-
<Card className="bg-muted/20 py-2 gap-2">
|
|
2136
|
-
<CardHeader className="px-3 pt-2 pb-0">
|
|
2137
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
2138
|
-
{t('structureEditor.videoProfiles.title')}
|
|
2139
|
-
</CardTitle>
|
|
2140
|
-
</CardHeader>
|
|
2141
|
-
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
2142
|
-
<EntityPicker
|
|
2143
|
-
key={videoProfilePickerResetKey}
|
|
2144
|
-
placeholder={t(
|
|
2145
|
-
'structureEditor.videoProfiles.placeholder'
|
|
2146
|
-
)}
|
|
2147
|
-
options={availableVideoProfiles}
|
|
2148
|
-
getOptionValue={(opt) => opt.id}
|
|
2149
|
-
getOptionLabel={(opt) => opt.name}
|
|
2150
|
-
onChange={(_val, opt) => {
|
|
2151
|
-
if (opt && !linkedProfileIds.includes(opt.id)) {
|
|
2152
|
-
setLinkedProfileIds((prev) => [...prev, opt.id]);
|
|
2153
|
-
setVideoProfilePickerResetKey(
|
|
2154
|
-
(current) => current + 1
|
|
2155
|
-
);
|
|
2156
|
-
}
|
|
2157
|
-
}}
|
|
2158
|
-
showCreateButton
|
|
2159
|
-
createTitle={t(
|
|
2160
|
-
'structureEditor.videoProfiles.createTitle'
|
|
2161
|
-
)}
|
|
2162
|
-
createFields={[
|
|
2163
|
-
{
|
|
2164
|
-
name: 'name',
|
|
2165
|
-
label: t(
|
|
2166
|
-
'structureEditor.videoProfiles.createFields.name.label'
|
|
2167
|
-
),
|
|
2168
|
-
placeholder: t(
|
|
2169
|
-
'structureEditor.videoProfiles.createFields.name.placeholder'
|
|
2170
|
-
),
|
|
2171
|
-
required: true,
|
|
2172
|
-
},
|
|
2173
|
-
{
|
|
2174
|
-
name: 'ffmpeg_params',
|
|
2175
|
-
label: t(
|
|
2176
|
-
'structureEditor.videoProfiles.createFields.ffmpegParams.label'
|
|
2177
|
-
),
|
|
2178
|
-
placeholder: t(
|
|
2179
|
-
'structureEditor.videoProfiles.createFields.ffmpegParams.placeholder'
|
|
2180
|
-
),
|
|
2181
|
-
required: true,
|
|
2182
|
-
},
|
|
2183
|
-
]}
|
|
2184
|
-
onCreate={async (values) => {
|
|
2185
|
-
try {
|
|
2186
|
-
const response = await request<VideoProfileOption>({
|
|
2187
|
-
url: '/lms/video-resolution-profiles',
|
|
2188
|
-
method: 'POST',
|
|
2189
|
-
data: {
|
|
2190
|
-
name: values.name,
|
|
2191
|
-
ffmpeg_params: values.ffmpeg_params,
|
|
2192
|
-
},
|
|
2193
|
-
});
|
|
2194
|
-
await refetchVideoProfiles();
|
|
2195
|
-
return response.data;
|
|
2196
|
-
} catch {
|
|
2197
|
-
toast.error(
|
|
2198
|
-
t('structureEditor.videoProfiles.createError')
|
|
2199
|
-
);
|
|
2200
|
-
return null;
|
|
2201
|
-
}
|
|
2202
|
-
}}
|
|
2203
|
-
/>
|
|
2204
|
-
|
|
2205
|
-
{isFetchingVideoProfiles ? (
|
|
2206
|
-
<div className="flex flex-col gap-1">
|
|
2207
|
-
{Array.from({ length: 3 }).map((_, index) => (
|
|
2208
|
-
<div
|
|
2209
|
-
key={`video-profile-skeleton-${index}`}
|
|
2210
|
-
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
|
|
2211
|
-
>
|
|
2212
|
-
<Skeleton className="size-3.5 rounded-full shrink-0" />
|
|
2213
|
-
<Skeleton className="h-3 w-full max-w-56" />
|
|
2214
|
-
</div>
|
|
2215
|
-
))}
|
|
2216
|
-
</div>
|
|
2217
|
-
) : linkedProfileIds.length === 0 ? (
|
|
2218
|
-
<p className="text-center text-xs text-muted-foreground py-1">
|
|
2219
|
-
{t('structureEditor.videoProfiles.empty')}
|
|
2220
|
-
</p>
|
|
2221
|
-
) : (
|
|
2222
|
-
<div className="flex flex-col gap-1">
|
|
2223
|
-
{linkedProfileIds.map((profileId) => {
|
|
2224
|
-
const profile = allVideoProfiles.find(
|
|
2225
|
-
(p) => p.id === profileId
|
|
2226
|
-
);
|
|
2227
|
-
return (
|
|
2228
|
-
<div
|
|
2229
|
-
key={profileId}
|
|
2230
|
-
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
|
|
2231
|
-
>
|
|
2232
|
-
<Video className="size-3.5 shrink-0 text-violet-500" />
|
|
2233
|
-
<div className="flex-1 min-w-0">
|
|
2234
|
-
<p className="text-xs font-medium truncate">
|
|
2235
|
-
{profile?.name?.trim()
|
|
2236
|
-
? profile.name
|
|
2237
|
-
: t(
|
|
2238
|
-
'structureEditor.videoProfiles.fallbackName',
|
|
2239
|
-
{
|
|
2240
|
-
id: profileId,
|
|
2241
|
-
}
|
|
2242
|
-
)}
|
|
2243
|
-
</p>
|
|
2244
|
-
</div>
|
|
2245
|
-
<IconActionTooltip
|
|
2246
|
-
label={t(
|
|
2247
|
-
'structureEditor.videoProfiles.editAria'
|
|
2248
|
-
)}
|
|
2249
|
-
>
|
|
2250
|
-
<Button
|
|
2251
|
-
type="button"
|
|
2252
|
-
variant="ghost"
|
|
2253
|
-
size="icon"
|
|
2254
|
-
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
2255
|
-
onClick={() =>
|
|
2256
|
-
handleEditVideoProfile(profileId)
|
|
2257
|
-
}
|
|
2258
|
-
aria-label={t(
|
|
2259
|
-
'structureEditor.videoProfiles.editAria'
|
|
2260
|
-
)}
|
|
2261
|
-
>
|
|
2262
|
-
<Pencil className="size-3" />
|
|
2263
|
-
</Button>
|
|
2264
|
-
</IconActionTooltip>
|
|
2265
|
-
<IconActionTooltip
|
|
2266
|
-
label={t(
|
|
2267
|
-
'structureEditor.videoProfiles.removeAria'
|
|
2268
|
-
)}
|
|
2269
|
-
>
|
|
2270
|
-
<Button
|
|
2271
|
-
type="button"
|
|
2272
|
-
variant="ghost"
|
|
2273
|
-
size="icon"
|
|
2274
|
-
className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
2275
|
-
onClick={() =>
|
|
2276
|
-
setLinkedProfileIds((prev) =>
|
|
2277
|
-
prev.filter((id) => id !== profileId)
|
|
2278
|
-
)
|
|
2279
|
-
}
|
|
2280
|
-
aria-label={t(
|
|
2281
|
-
'structureEditor.videoProfiles.removeAria'
|
|
2282
|
-
)}
|
|
2283
|
-
>
|
|
2284
|
-
<X className="size-3" />
|
|
2285
|
-
</Button>
|
|
2286
|
-
</IconActionTooltip>
|
|
2287
|
-
</div>
|
|
2288
|
-
);
|
|
2289
|
-
})}
|
|
2290
|
-
</div>
|
|
2291
|
-
)}
|
|
2292
|
-
</CardContent>
|
|
2293
|
-
</Card>
|
|
2294
|
-
</TabsContent>
|
|
2295
|
-
)}
|
|
2296
|
-
|
|
2297
2492
|
{/* ── Tab: Operações ──────────────────────────────────────── */}
|
|
2298
2493
|
{showOperationsTab && (
|
|
2299
2494
|
<TabsContent value="operacoes" className="mt-0 min-w-0">
|
|
@@ -2356,21 +2551,39 @@ export function EditorCourse() {
|
|
|
2356
2551
|
{t('structureEditor.publishChecklist.statusTitle')}
|
|
2357
2552
|
</CardTitle>
|
|
2358
2553
|
</CardHeader>
|
|
2359
|
-
<CardContent className="px-3 pb-2 flex
|
|
2360
|
-
<
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
{isReadyToPublish
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2554
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
2555
|
+
<div className="flex items-center justify-between gap-2">
|
|
2556
|
+
<p className="text-xs text-muted-foreground">
|
|
2557
|
+
{t('structureEditor.publishChecklist.statusProgress', {
|
|
2558
|
+
completed: completedRequired,
|
|
2559
|
+
total: requiredChecklist.length,
|
|
2560
|
+
})}
|
|
2561
|
+
</p>
|
|
2562
|
+
<Badge variant={isReadyToPublish ? 'default' : 'secondary'}>
|
|
2563
|
+
{isReadyToPublish
|
|
2564
|
+
? t('structureEditor.publishChecklist.ready')
|
|
2565
|
+
: t('structureEditor.publishChecklist.notReady')}
|
|
2566
|
+
</Badge>
|
|
2567
|
+
</div>
|
|
2568
|
+
<Button
|
|
2569
|
+
type="button"
|
|
2570
|
+
size="sm"
|
|
2571
|
+
className="w-full h-8 text-xs"
|
|
2572
|
+
disabled={!isReadyToPublish || formValues.status === 'publicado' || saving}
|
|
2573
|
+
onClick={handlePublish}
|
|
2574
|
+
>
|
|
2575
|
+
{formValues.status === 'publicado'
|
|
2576
|
+
? t('structureEditor.publishChecklist.alreadyPublished')
|
|
2577
|
+
: t('structureEditor.publishChecklist.publishButton')}
|
|
2578
|
+
</Button>
|
|
2371
2579
|
</CardContent>
|
|
2372
2580
|
</Card>
|
|
2373
2581
|
</TabsContent>
|
|
2582
|
+
|
|
2583
|
+
{/* ── Tab: Exportações ────────────────────────────────────────── */}
|
|
2584
|
+
<TabsContent value="exportacoes" className="mt-0 min-w-0">
|
|
2585
|
+
<CourseExportsTab courseId={courseId} />
|
|
2586
|
+
</TabsContent>
|
|
2374
2587
|
</div>
|
|
2375
2588
|
</div>
|
|
2376
2589
|
</Tabs>
|
|
@@ -2419,68 +2632,6 @@ export function EditorCourse() {
|
|
|
2419
2632
|
</div>
|
|
2420
2633
|
</form>
|
|
2421
2634
|
|
|
2422
|
-
<Sheet
|
|
2423
|
-
open={videoProfileEditSheetOpen}
|
|
2424
|
-
onOpenChange={(open) => {
|
|
2425
|
-
setVideoProfileEditSheetOpen(open);
|
|
2426
|
-
if (!open) setEditingVideoProfileId(null);
|
|
2427
|
-
}}
|
|
2428
|
-
>
|
|
2429
|
-
<ResizableSheetContent
|
|
2430
|
-
sheetId="lms-course-structure-video-profile-edit-sheet"
|
|
2431
|
-
defaultWidth={560}
|
|
2432
|
-
minWidth={420}
|
|
2433
|
-
maxWidth={920}
|
|
2434
|
-
className="sm:max-w-lg"
|
|
2435
|
-
>
|
|
2436
|
-
<SheetHeader>
|
|
2437
|
-
<SheetTitle>
|
|
2438
|
-
{t('structureEditor.videoProfiles.sheet.title')}
|
|
2439
|
-
</SheetTitle>
|
|
2440
|
-
<SheetDescription>
|
|
2441
|
-
{t('structureEditor.videoProfiles.sheet.description')}
|
|
2442
|
-
</SheetDescription>
|
|
2443
|
-
</SheetHeader>
|
|
2444
|
-
<div className="space-y-3 px-4 pb-4 pt-2">
|
|
2445
|
-
<div className="space-y-1.5">
|
|
2446
|
-
<FormLabel>
|
|
2447
|
-
{t('structureEditor.videoProfiles.createFields.name.label')}
|
|
2448
|
-
</FormLabel>
|
|
2449
|
-
<Input
|
|
2450
|
-
value={editingVideoProfileName}
|
|
2451
|
-
onChange={(e) => setEditingVideoProfileName(e.target.value)}
|
|
2452
|
-
placeholder={t(
|
|
2453
|
-
'structureEditor.videoProfiles.createFields.name.placeholder'
|
|
2454
|
-
)}
|
|
2455
|
-
/>
|
|
2456
|
-
</div>
|
|
2457
|
-
<FfmpegParamsEditor
|
|
2458
|
-
value={editingVideoProfileParams}
|
|
2459
|
-
onChange={setEditingVideoProfileParams}
|
|
2460
|
-
/>
|
|
2461
|
-
<div className="flex justify-end gap-2 pt-2">
|
|
2462
|
-
<Button
|
|
2463
|
-
type="button"
|
|
2464
|
-
variant="outline"
|
|
2465
|
-
onClick={() => setVideoProfileEditSheetOpen(false)}
|
|
2466
|
-
disabled={savingVideoProfileEdit}
|
|
2467
|
-
>
|
|
2468
|
-
{t('structureEditor.videoProfiles.sheet.actions.cancel')}
|
|
2469
|
-
</Button>
|
|
2470
|
-
<Button
|
|
2471
|
-
type="button"
|
|
2472
|
-
disabled={savingVideoProfileEdit}
|
|
2473
|
-
onClick={() => void handleSaveVideoProfileEdit()}
|
|
2474
|
-
>
|
|
2475
|
-
{savingVideoProfileEdit
|
|
2476
|
-
? t('structureEditor.videoProfiles.sheet.actions.saving')
|
|
2477
|
-
: t('structureEditor.videoProfiles.sheet.actions.save')}
|
|
2478
|
-
</Button>
|
|
2479
|
-
</div>
|
|
2480
|
-
</div>
|
|
2481
|
-
</ResizableSheetContent>
|
|
2482
|
-
</Sheet>
|
|
2483
|
-
|
|
2484
2635
|
<Sheet
|
|
2485
2636
|
open={categoryEditSheetOpen}
|
|
2486
2637
|
onOpenChange={(open) => {
|
|
@@ -2556,53 +2707,40 @@ export function EditorCourse() {
|
|
|
2556
2707
|
</ResizableSheetContent>
|
|
2557
2708
|
</Sheet>
|
|
2558
2709
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
</Button>
|
|
2594
|
-
<Button
|
|
2595
|
-
variant="destructive"
|
|
2596
|
-
onClick={handleDelete}
|
|
2597
|
-
disabled={deleting}
|
|
2598
|
-
className="gap-2"
|
|
2599
|
-
>
|
|
2600
|
-
{deleting ? <Loader2 className="size-4 animate-spin" /> : null}
|
|
2601
|
-
{t('deleteDialog.actions.delete')}
|
|
2602
|
-
</Button>
|
|
2603
|
-
</DialogFooter>
|
|
2604
|
-
</DialogContent>
|
|
2605
|
-
</Dialog>
|
|
2710
|
+
<CourseExportSheet
|
|
2711
|
+
open={exportSheetOpen}
|
|
2712
|
+
onOpenChange={setExportSheetOpen}
|
|
2713
|
+
courseId={courseId}
|
|
2714
|
+
sessionCount={sessions.length}
|
|
2715
|
+
videoLessonCount={lessons.filter((l) => l.type === 'video').length}
|
|
2716
|
+
/>
|
|
2717
|
+
|
|
2718
|
+
<CourseDeleteDialog
|
|
2719
|
+
open={deleteDialogOpen}
|
|
2720
|
+
onOpenChange={(open) => {
|
|
2721
|
+
setDeleteDialogOpen(open);
|
|
2722
|
+
if (!open) {
|
|
2723
|
+
setConfirmationInput('');
|
|
2724
|
+
setDeleteImpact(null);
|
|
2725
|
+
}
|
|
2726
|
+
}}
|
|
2727
|
+
course={{
|
|
2728
|
+
title: deleteCourseTitle || course.title,
|
|
2729
|
+
status: deleteCourseStatus ?? 'rascunho',
|
|
2730
|
+
enrollmentCount:
|
|
2731
|
+
deleteImpact?.enrollmentCount ?? apiCourse?.enrollmentCount ?? 0,
|
|
2732
|
+
}}
|
|
2733
|
+
impact={deleteImpact}
|
|
2734
|
+
loadingImpact={loadingDeleteImpact}
|
|
2735
|
+
confirmationInput={confirmationInput}
|
|
2736
|
+
onConfirmationInputChange={setConfirmationInput}
|
|
2737
|
+
archiving={archivingCourse}
|
|
2738
|
+
queuingDelete={queuingDelete}
|
|
2739
|
+
canDelete={canDeleteCourse}
|
|
2740
|
+
onArchive={archiveCourseForDelete}
|
|
2741
|
+
onDelete={handleDelete}
|
|
2742
|
+
t={courseListT}
|
|
2743
|
+
/>
|
|
2606
2744
|
</Form>
|
|
2607
2745
|
);
|
|
2608
2746
|
}
|