@hed-hog/lms 0.0.361 → 0.0.364
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 +65 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -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-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 +11 -4
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +14 -0
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +15 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +139 -4
- 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.controller.d.ts +73 -1
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +27 -3
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +2 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +108 -4
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +631 -30
- 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 +33 -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 +4 -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 +21 -0
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.js +443 -0
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.js +277 -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 +116 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +71 -2
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +142 -3
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +606 -21
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +4 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +21 -6
- package/dist/course/lms-setting.controller.js.map +1 -1
- 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 +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 +8 -8
- 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 +26 -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 +304 -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.module.d.ts.map +1 -1
- package/dist/lms.module.js +17 -2
- 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.controller.d.ts +88 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +85 -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/student-xp.controller.d.ts +41 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
- package/dist/student-xp/student-xp.controller.js +114 -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 +65 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -0
- package/dist/student-xp/student-xp.service.js +197 -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/menu.yaml +101 -0
- package/hedhog/data/route.yaml +512 -0
- package/hedhog/data/setting_group.yaml +1 -1
- 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 +1453 -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 +4 -4
- 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 +1170 -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 +55 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
- 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 +21 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
- 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/use-course-content-overview.ts.ejs +54 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
- 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-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 +400 -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 +386 -3
- package/hedhog/frontend/messages/pt.json +386 -3
- 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 +8 -7
- package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -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-lesson.controller.ts +6 -1
- package/src/course/course-structure.controller.ts +10 -0
- package/src/course/course-structure.service.ts +174 -1
- package/src/course/course-video-conversion.service.ts +113 -75
- package/src/course/course.controller.ts +22 -3
- package/src/course/course.module.ts +2 -0
- package/src/course/course.service.ts +847 -30
- package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
- package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
- package/src/course/lms-bulk-upload-automation.service.ts +560 -0
- package/src/course/lms-bulk-upload-infra.service.ts +327 -0
- package/src/course/lms-bulk-upload.constants.ts +5 -0
- package/src/course/lms-bulk-upload.controller.ts +79 -3
- package/src/course/lms-bulk-upload.service.ts +1029 -204
- package/src/course/lms-setting.controller.ts +22 -6
- 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 +396 -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.module.ts +17 -2
- package/src/platforma/dto/update-profile.dto.ts +59 -0
- package/src/platforma/platforma.controller.ts +57 -2
- package/src/platforma/platforma.service.ts +268 -0
- package/src/student-xp/student-xp.controller.ts +76 -0
- package/src/student-xp/student-xp.module.ts +12 -0
- package/src/student-xp/student-xp.service.ts +236 -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
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
4
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import {
|
|
8
|
+
AlertDialog,
|
|
9
|
+
AlertDialogAction,
|
|
10
|
+
AlertDialogCancel,
|
|
11
|
+
AlertDialogContent,
|
|
12
|
+
AlertDialogDescription,
|
|
13
|
+
AlertDialogFooter,
|
|
14
|
+
AlertDialogHeader,
|
|
15
|
+
AlertDialogTitle,
|
|
16
|
+
} from '@/components/ui/alert-dialog';
|
|
17
|
+
import {
|
|
18
|
+
Card,
|
|
19
|
+
CardContent,
|
|
20
|
+
CardDescription,
|
|
21
|
+
CardHeader,
|
|
22
|
+
CardTitle,
|
|
23
|
+
} from '@/components/ui/card';
|
|
24
|
+
import {
|
|
25
|
+
ChartContainer,
|
|
26
|
+
ChartTooltip,
|
|
27
|
+
ChartTooltipContent,
|
|
28
|
+
} from '@/components/ui/chart';
|
|
29
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
30
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
31
|
+
import {
|
|
32
|
+
Download,
|
|
33
|
+
BookOpen,
|
|
34
|
+
Clapperboard,
|
|
35
|
+
Loader2,
|
|
36
|
+
FileText,
|
|
37
|
+
Film,
|
|
38
|
+
HardDrive,
|
|
39
|
+
Image,
|
|
40
|
+
Layers,
|
|
41
|
+
Mic,
|
|
42
|
+
Paperclip,
|
|
43
|
+
Sparkles,
|
|
44
|
+
Target,
|
|
45
|
+
Trash2,
|
|
46
|
+
type LucideIcon,
|
|
47
|
+
Zap,
|
|
48
|
+
} from 'lucide-react';
|
|
49
|
+
import { useTranslations } from 'next-intl';
|
|
50
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
51
|
+
import { Cell, Pie, PieChart } from 'recharts';
|
|
52
|
+
import { toast } from 'sonner';
|
|
53
|
+
import { useCourseContentOverviewQuery } from '../_data/use-course-content-overview';
|
|
54
|
+
import { useCourseXpOverviewQuery } from '../_data/use-course-xp-overview';
|
|
55
|
+
|
|
56
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
57
|
+
video: '#0ea5e9',
|
|
58
|
+
questao: '#f97316',
|
|
59
|
+
post: '#22c55e',
|
|
60
|
+
exercicio: '#a855f7',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const TYPE_GRADIENTS: Record<
|
|
64
|
+
string,
|
|
65
|
+
{ from: string; icon: LucideIcon; label: string }
|
|
66
|
+
> = {
|
|
67
|
+
video: {
|
|
68
|
+
from: 'from-sky-500/12 via-blue-500/6',
|
|
69
|
+
icon: Clapperboard,
|
|
70
|
+
label: 'Vídeo',
|
|
71
|
+
},
|
|
72
|
+
questao: {
|
|
73
|
+
from: 'from-orange-500/12 via-amber-500/6',
|
|
74
|
+
icon: FileText,
|
|
75
|
+
label: 'Questão',
|
|
76
|
+
},
|
|
77
|
+
post: {
|
|
78
|
+
from: 'from-green-500/12 via-emerald-500/6',
|
|
79
|
+
icon: BookOpen,
|
|
80
|
+
label: 'Post',
|
|
81
|
+
},
|
|
82
|
+
exercicio: {
|
|
83
|
+
from: 'from-violet-500/12 via-fuchsia-500/6',
|
|
84
|
+
icon: Target,
|
|
85
|
+
label: 'Exercício',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const AREA_COLORS = ['#0f766e', '#0ea5e9', '#14b8a6', '#38bdf8', '#0891b2'];
|
|
90
|
+
const SKILL_COLORS = ['#ea580c', '#f97316', '#fb923c', '#fdba74', '#fed7aa'];
|
|
91
|
+
|
|
92
|
+
const STORAGE_CATEGORY_META: Record<
|
|
93
|
+
string,
|
|
94
|
+
{
|
|
95
|
+
label: string;
|
|
96
|
+
accentClassName: string;
|
|
97
|
+
icon: LucideIcon;
|
|
98
|
+
iconClassName: string;
|
|
99
|
+
iconBackgroundClassName: string;
|
|
100
|
+
}
|
|
101
|
+
> = {
|
|
102
|
+
video_original: {
|
|
103
|
+
label: 'Vídeos originais',
|
|
104
|
+
accentClassName: 'from-sky-500/20 via-blue-500/10 to-transparent',
|
|
105
|
+
icon: Film,
|
|
106
|
+
iconClassName: 'text-sky-600 dark:text-sky-400',
|
|
107
|
+
iconBackgroundClassName: 'bg-sky-500/10',
|
|
108
|
+
},
|
|
109
|
+
video_profile: {
|
|
110
|
+
label: 'Vídeos convertidos',
|
|
111
|
+
accentClassName: 'from-indigo-500/20 via-blue-500/10 to-transparent',
|
|
112
|
+
icon: Clapperboard,
|
|
113
|
+
iconClassName: 'text-indigo-600 dark:text-indigo-400',
|
|
114
|
+
iconBackgroundClassName: 'bg-indigo-500/10',
|
|
115
|
+
},
|
|
116
|
+
lesson_audio: {
|
|
117
|
+
label: 'Áudios',
|
|
118
|
+
accentClassName: 'from-teal-500/20 via-cyan-500/10 to-transparent',
|
|
119
|
+
icon: Mic,
|
|
120
|
+
iconClassName: 'text-teal-600 dark:text-teal-400',
|
|
121
|
+
iconBackgroundClassName: 'bg-teal-500/10',
|
|
122
|
+
},
|
|
123
|
+
extracted_image: {
|
|
124
|
+
label: 'Imagens extraídas',
|
|
125
|
+
accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
|
|
126
|
+
icon: Image,
|
|
127
|
+
iconClassName: 'text-amber-600 dark:text-amber-400',
|
|
128
|
+
iconBackgroundClassName: 'bg-amber-500/10',
|
|
129
|
+
},
|
|
130
|
+
student_download: {
|
|
131
|
+
label: 'Downloads do aluno',
|
|
132
|
+
accentClassName: 'from-violet-500/20 via-fuchsia-500/10 to-transparent',
|
|
133
|
+
icon: Download,
|
|
134
|
+
iconClassName: 'text-violet-600 dark:text-violet-400',
|
|
135
|
+
iconBackgroundClassName: 'bg-violet-500/10',
|
|
136
|
+
},
|
|
137
|
+
supplementary_material: {
|
|
138
|
+
label: 'Materiais de apoio',
|
|
139
|
+
accentClassName: 'from-fuchsia-500/20 via-pink-500/10 to-transparent',
|
|
140
|
+
icon: Paperclip,
|
|
141
|
+
iconClassName: 'text-fuchsia-600 dark:text-fuchsia-400',
|
|
142
|
+
iconBackgroundClassName: 'bg-fuchsia-500/10',
|
|
143
|
+
},
|
|
144
|
+
course_image: {
|
|
145
|
+
label: 'Imagens do curso',
|
|
146
|
+
accentClassName: 'from-rose-500/20 via-pink-500/10 to-transparent',
|
|
147
|
+
icon: Image,
|
|
148
|
+
iconClassName: 'text-rose-600 dark:text-rose-400',
|
|
149
|
+
iconBackgroundClassName: 'bg-rose-500/10',
|
|
150
|
+
},
|
|
151
|
+
course_file: {
|
|
152
|
+
label: 'Arquivos do curso',
|
|
153
|
+
accentClassName: 'from-slate-500/20 via-zinc-500/10 to-transparent',
|
|
154
|
+
icon: FileText,
|
|
155
|
+
iconClassName: 'text-slate-600 dark:text-slate-400',
|
|
156
|
+
iconBackgroundClassName: 'bg-slate-500/10',
|
|
157
|
+
},
|
|
158
|
+
other_lesson_file: {
|
|
159
|
+
label: 'Outros arquivos',
|
|
160
|
+
accentClassName: 'from-muted/80 via-muted/40 to-transparent',
|
|
161
|
+
icon: HardDrive,
|
|
162
|
+
iconClassName: 'text-muted-foreground',
|
|
163
|
+
iconBackgroundClassName: 'bg-muted',
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
interface Props {
|
|
168
|
+
courseId: string;
|
|
169
|
+
locale: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type CleanupDialogState = {
|
|
173
|
+
categoryKey: string;
|
|
174
|
+
categoryLabel: string;
|
|
175
|
+
fileCount: number;
|
|
176
|
+
totalBytes: number;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
type CleanupTrackedJobState = {
|
|
180
|
+
categoryKey: string;
|
|
181
|
+
queueJobId: number;
|
|
182
|
+
status: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export function CourseOverviewTab({ courseId, locale }: Props) {
|
|
186
|
+
const { request } = useApp();
|
|
187
|
+
const queryClient = useQueryClient();
|
|
188
|
+
const t = useTranslations('lms.CursoEditPage.structureEditor.courseOverview');
|
|
189
|
+
const { data, isLoading, isError } = useCourseContentOverviewQuery(courseId);
|
|
190
|
+
const { data: xpData } = useCourseXpOverviewQuery(courseId);
|
|
191
|
+
const [cleanupCategoryKey, setCleanupCategoryKey] = useState<string | null>(
|
|
192
|
+
null
|
|
193
|
+
);
|
|
194
|
+
const [cleanupDialogState, setCleanupDialogState] =
|
|
195
|
+
useState<CleanupDialogState | null>(null);
|
|
196
|
+
const [cleanupTrackedJob, setCleanupTrackedJob] =
|
|
197
|
+
useState<CleanupTrackedJobState | null>(null);
|
|
198
|
+
|
|
199
|
+
const { data: cleanupTrackedJobDetail, isError: isCleanupTrackedJobError } =
|
|
200
|
+
useQuery<{
|
|
201
|
+
id: number;
|
|
202
|
+
status: string;
|
|
203
|
+
last_error?: string | null;
|
|
204
|
+
} | null>({
|
|
205
|
+
queryKey: [
|
|
206
|
+
'lms-course-storage-cleanup-job',
|
|
207
|
+
cleanupTrackedJob?.queueJobId,
|
|
208
|
+
],
|
|
209
|
+
enabled: Boolean(cleanupTrackedJob?.queueJobId),
|
|
210
|
+
queryFn: async () => {
|
|
211
|
+
if (!cleanupTrackedJob?.queueJobId) return null;
|
|
212
|
+
const response = await request<{
|
|
213
|
+
id: number;
|
|
214
|
+
status: string;
|
|
215
|
+
last_error?: string | null;
|
|
216
|
+
}>({
|
|
217
|
+
url: `/queue/jobs/${cleanupTrackedJob.queueJobId}`,
|
|
218
|
+
method: 'GET',
|
|
219
|
+
});
|
|
220
|
+
return response.data;
|
|
221
|
+
},
|
|
222
|
+
refetchInterval: ({ state }) => {
|
|
223
|
+
const status = (state.data as { status?: string } | null)?.status;
|
|
224
|
+
if (!status) return 3000;
|
|
225
|
+
return ['pending', 'scheduled', 'processing', 'retrying'].includes(
|
|
226
|
+
status
|
|
227
|
+
)
|
|
228
|
+
? 3000
|
|
229
|
+
: false;
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const formatBytes = (bytes: number) => {
|
|
234
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
235
|
+
return '0 B';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
239
|
+
let value = bytes;
|
|
240
|
+
let unitIndex = 0;
|
|
241
|
+
|
|
242
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
243
|
+
value /= 1024;
|
|
244
|
+
unitIndex += 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const digits = unitIndex === 0 ? 0 : value >= 10 ? 1 : 2;
|
|
248
|
+
return `${new Intl.NumberFormat(locale || 'pt-BR', {
|
|
249
|
+
minimumFractionDigits: 0,
|
|
250
|
+
maximumFractionDigits: digits,
|
|
251
|
+
}).format(value)} ${units[unitIndex]}`;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const kpiItems = useMemo(() => {
|
|
255
|
+
if (!data) return [];
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
key: 'modules',
|
|
259
|
+
title: t('kpis.modules'),
|
|
260
|
+
value: data.structure.moduleCount,
|
|
261
|
+
description: t('kpis.modulesDescription'),
|
|
262
|
+
icon: Layers,
|
|
263
|
+
accentClassName: 'from-blue-500 via-indigo-500 to-violet-500',
|
|
264
|
+
iconContainerClassName:
|
|
265
|
+
'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: 'lessons',
|
|
269
|
+
title: t('kpis.lessons'),
|
|
270
|
+
value: data.structure.lessonCount,
|
|
271
|
+
description: t('kpis.lessonsDescription'),
|
|
272
|
+
icon: BookOpen,
|
|
273
|
+
accentClassName: 'from-emerald-500 via-teal-500 to-green-500',
|
|
274
|
+
iconContainerClassName:
|
|
275
|
+
'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
key: 'images',
|
|
279
|
+
title: t('kpis.extractedImages'),
|
|
280
|
+
value: data.media.extractedImageCount,
|
|
281
|
+
description: t('kpis.extractedImagesDescription'),
|
|
282
|
+
icon: Image,
|
|
283
|
+
accentClassName: 'from-amber-500 via-orange-500 to-yellow-500',
|
|
284
|
+
iconContainerClassName:
|
|
285
|
+
'bg-amber-500/10 text-amber-600 dark:text-amber-400',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
key: 'resources',
|
|
289
|
+
title: t('kpis.resourceFiles'),
|
|
290
|
+
value: data.resources.fileCount,
|
|
291
|
+
description: t('kpis.resourceFilesDescription'),
|
|
292
|
+
icon: Paperclip,
|
|
293
|
+
accentClassName: 'from-violet-500 via-fuchsia-500 to-purple-500',
|
|
294
|
+
iconContainerClassName:
|
|
295
|
+
'bg-violet-500/10 text-violet-600 dark:text-violet-400',
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
}, [data, t]);
|
|
299
|
+
|
|
300
|
+
const lessonTypeData = useMemo(() => {
|
|
301
|
+
if (!data) return [];
|
|
302
|
+
const types = ['video', 'questao', 'post', 'exercicio'] as const;
|
|
303
|
+
return types
|
|
304
|
+
.map((type) => ({
|
|
305
|
+
type,
|
|
306
|
+
count: data.structure.lessonsByType[type],
|
|
307
|
+
color: TYPE_COLORS[type],
|
|
308
|
+
label: TYPE_GRADIENTS[type]!.label,
|
|
309
|
+
}))
|
|
310
|
+
.filter((item) => item.count > 0);
|
|
311
|
+
}, [data]);
|
|
312
|
+
|
|
313
|
+
const lessonTypeChartConfig = useMemo(
|
|
314
|
+
() =>
|
|
315
|
+
Object.fromEntries(
|
|
316
|
+
lessonTypeData.map((item) => [
|
|
317
|
+
item.type,
|
|
318
|
+
{ label: item.label, color: item.color },
|
|
319
|
+
])
|
|
320
|
+
),
|
|
321
|
+
[lessonTypeData]
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const storageCategories = useMemo(() => {
|
|
325
|
+
if (!data) return [];
|
|
326
|
+
|
|
327
|
+
return data.storage.categories.map((category) => {
|
|
328
|
+
const meta = (STORAGE_CATEGORY_META[category.key] ??
|
|
329
|
+
STORAGE_CATEGORY_META.other_lesson_file)!;
|
|
330
|
+
const sharePercent =
|
|
331
|
+
data.storage.totalBytes > 0
|
|
332
|
+
? (category.totalBytes / data.storage.totalBytes) * 100
|
|
333
|
+
: 0;
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
...category,
|
|
337
|
+
...meta,
|
|
338
|
+
sharePercent,
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}, [data]);
|
|
342
|
+
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!cleanupTrackedJob || !cleanupTrackedJobDetail) return;
|
|
345
|
+
|
|
346
|
+
const status = cleanupTrackedJobDetail.status;
|
|
347
|
+
|
|
348
|
+
setCleanupTrackedJob((prev) => {
|
|
349
|
+
if (!prev) return prev;
|
|
350
|
+
if (prev.status === status) return prev;
|
|
351
|
+
return { ...prev, status };
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (status === 'completed') {
|
|
355
|
+
toast.success('Limpeza concluída com sucesso.');
|
|
356
|
+
void queryClient.invalidateQueries({
|
|
357
|
+
queryKey: ['course-content-overview', courseId],
|
|
358
|
+
});
|
|
359
|
+
setCleanupTrackedJob(null);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (['failed', 'dead_letter', 'canceled'].includes(status)) {
|
|
364
|
+
toast.error(
|
|
365
|
+
cleanupTrackedJobDetail.last_error ||
|
|
366
|
+
'A limpeza falhou. Você pode tentar excluir novamente.'
|
|
367
|
+
);
|
|
368
|
+
setCleanupTrackedJob(null);
|
|
369
|
+
}
|
|
370
|
+
}, [cleanupTrackedJob, cleanupTrackedJobDetail, courseId, queryClient]);
|
|
371
|
+
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (!cleanupTrackedJob || !isCleanupTrackedJobError) return;
|
|
374
|
+
|
|
375
|
+
toast.error('Não foi possível acompanhar o status da limpeza.');
|
|
376
|
+
setCleanupTrackedJob(null);
|
|
377
|
+
}, [cleanupTrackedJob, isCleanupTrackedJobError]);
|
|
378
|
+
|
|
379
|
+
if (isLoading) {
|
|
380
|
+
return (
|
|
381
|
+
<div className="flex flex-col gap-3">
|
|
382
|
+
<div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
|
|
383
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
384
|
+
<Skeleton key={i} className="h-28 rounded-xl" />
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
<div className="grid gap-3 xl:grid-cols-2">
|
|
388
|
+
<Skeleton className="h-72 rounded-xl" />
|
|
389
|
+
<Skeleton className="h-72 rounded-xl" />
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (isError || !data) {
|
|
396
|
+
return (
|
|
397
|
+
<Card className="border-dashed bg-muted/20">
|
|
398
|
+
<CardHeader>
|
|
399
|
+
<CardTitle className="text-sm font-semibold">
|
|
400
|
+
{t('errorTitle')}
|
|
401
|
+
</CardTitle>
|
|
402
|
+
<CardDescription>{t('errorDescription')}</CardDescription>
|
|
403
|
+
</CardHeader>
|
|
404
|
+
</Card>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const totalLessons = data.structure.lessonCount;
|
|
409
|
+
const videoCount = data.videos.lessonCount;
|
|
410
|
+
const overviewHighlights = [
|
|
411
|
+
{
|
|
412
|
+
key: 'storage',
|
|
413
|
+
label: 'Armazenamento',
|
|
414
|
+
value: formatBytes(data.storage.totalBytes),
|
|
415
|
+
toneClassName:
|
|
416
|
+
'border-cyan-500/15 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300',
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
key: 'coverage',
|
|
420
|
+
label: 'Cobertura de vídeo',
|
|
421
|
+
value:
|
|
422
|
+
totalLessons > 0
|
|
423
|
+
? `${Math.round((videoCount / totalLessons) * 100)}% das aulas`
|
|
424
|
+
: '0% das aulas',
|
|
425
|
+
toneClassName:
|
|
426
|
+
'border-teal-500/15 bg-teal-500/10 text-teal-700 dark:text-teal-300',
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
key: 'images',
|
|
430
|
+
label: 'Imagens extraídas',
|
|
431
|
+
value: `${data.media.extractedImageCount}`,
|
|
432
|
+
toneClassName:
|
|
433
|
+
'border-amber-500/15 bg-amber-500/10 text-amber-700 dark:text-amber-300',
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
key: 'resources',
|
|
437
|
+
label: 'Arquivos de apoio',
|
|
438
|
+
value: `${data.resources.fileCount}`,
|
|
439
|
+
toneClassName:
|
|
440
|
+
'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300',
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
const openCleanupCategoryDialog = (
|
|
445
|
+
categoryKey: string,
|
|
446
|
+
categoryLabel: string,
|
|
447
|
+
fileCount: number,
|
|
448
|
+
totalBytes: number
|
|
449
|
+
) => {
|
|
450
|
+
if (fileCount <= 0 || cleanupCategoryKey) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
setCleanupDialogState({
|
|
455
|
+
categoryKey,
|
|
456
|
+
categoryLabel,
|
|
457
|
+
fileCount,
|
|
458
|
+
totalBytes,
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const handleCleanupCategory = async () => {
|
|
463
|
+
if (!cleanupDialogState) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const { categoryKey, categoryLabel } = cleanupDialogState;
|
|
468
|
+
|
|
469
|
+
setCleanupDialogState(null);
|
|
470
|
+
|
|
471
|
+
setCleanupCategoryKey(categoryKey);
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const response = await request<{
|
|
475
|
+
status?: string;
|
|
476
|
+
queueJobId?: number;
|
|
477
|
+
queueJobStatus?: string;
|
|
478
|
+
cleanupImpact?: { formattedSize?: string };
|
|
479
|
+
}>({
|
|
480
|
+
url: `/lms/courses/${courseId}/storage/cleanup`,
|
|
481
|
+
method: 'POST',
|
|
482
|
+
data: { category: categoryKey },
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (response.data?.status === 'empty') {
|
|
486
|
+
toast.info(
|
|
487
|
+
`A categoria "${categoryLabel}" não possui arquivos para limpeza.`
|
|
488
|
+
);
|
|
489
|
+
} else {
|
|
490
|
+
if (response.data?.queueJobId) {
|
|
491
|
+
setCleanupTrackedJob({
|
|
492
|
+
categoryKey,
|
|
493
|
+
queueJobId: response.data.queueJobId,
|
|
494
|
+
status: response.data.queueJobStatus || 'pending',
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
toast.success(
|
|
498
|
+
`Limpeza da categoria "${categoryLabel}" enfileirada com sucesso.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
await queryClient.invalidateQueries({
|
|
503
|
+
queryKey: ['course-content-overview', courseId],
|
|
504
|
+
});
|
|
505
|
+
} catch (error: unknown) {
|
|
506
|
+
const responseMessage =
|
|
507
|
+
typeof error === 'object' &&
|
|
508
|
+
error !== null &&
|
|
509
|
+
'response' in error &&
|
|
510
|
+
typeof (error as { response?: { data?: { message?: unknown } } })
|
|
511
|
+
.response?.data?.message === 'string'
|
|
512
|
+
? (error as { response?: { data?: { message?: string } } }).response
|
|
513
|
+
?.data?.message
|
|
514
|
+
: undefined;
|
|
515
|
+
const message =
|
|
516
|
+
responseMessage ||
|
|
517
|
+
(error instanceof Error ? error.message : undefined) ||
|
|
518
|
+
'Não foi possível enfileirar a limpeza da categoria.';
|
|
519
|
+
toast.error(String(message));
|
|
520
|
+
} finally {
|
|
521
|
+
setCleanupCategoryKey(null);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
return (
|
|
526
|
+
<>
|
|
527
|
+
<AlertDialog
|
|
528
|
+
open={Boolean(cleanupDialogState)}
|
|
529
|
+
onOpenChange={(open) => {
|
|
530
|
+
if (!open && !cleanupCategoryKey) {
|
|
531
|
+
setCleanupDialogState(null);
|
|
532
|
+
}
|
|
533
|
+
}}
|
|
534
|
+
>
|
|
535
|
+
<AlertDialogContent>
|
|
536
|
+
<AlertDialogHeader>
|
|
537
|
+
<AlertDialogTitle>Confirmar exclusão da categoria</AlertDialogTitle>
|
|
538
|
+
<AlertDialogDescription>
|
|
539
|
+
{cleanupDialogState
|
|
540
|
+
? `Confirma excluir todos os ${cleanupDialogState.fileCount} arquivo(s) da categoria "${cleanupDialogState.categoryLabel}" deste curso?`
|
|
541
|
+
: 'Confirme a exclusão dos arquivos desta categoria.'}
|
|
542
|
+
{cleanupDialogState
|
|
543
|
+
? ` Espaço estimado a liberar: ${formatBytes(cleanupDialogState.totalBytes)}.`
|
|
544
|
+
: ''}{' '}
|
|
545
|
+
A remoção será processada em background via fila.
|
|
546
|
+
</AlertDialogDescription>
|
|
547
|
+
</AlertDialogHeader>
|
|
548
|
+
<AlertDialogFooter>
|
|
549
|
+
<AlertDialogCancel disabled={cleanupCategoryKey !== null}>
|
|
550
|
+
Cancelar
|
|
551
|
+
</AlertDialogCancel>
|
|
552
|
+
<AlertDialogAction
|
|
553
|
+
disabled={cleanupCategoryKey !== null}
|
|
554
|
+
onClick={(event) => {
|
|
555
|
+
event.preventDefault();
|
|
556
|
+
void handleCleanupCategory();
|
|
557
|
+
}}
|
|
558
|
+
className="bg-red-600 text-white hover:bg-red-700"
|
|
559
|
+
>
|
|
560
|
+
{cleanupCategoryKey !== null ? 'Enfileirando...' : 'Excluir'}
|
|
561
|
+
</AlertDialogAction>
|
|
562
|
+
</AlertDialogFooter>
|
|
563
|
+
</AlertDialogContent>
|
|
564
|
+
</AlertDialog>
|
|
565
|
+
|
|
566
|
+
<div className="flex flex-col gap-2.5">
|
|
567
|
+
<div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-2.5 py-2 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-3 sm:py-2.5">
|
|
568
|
+
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/50 pb-2">
|
|
569
|
+
<div className="min-w-0">
|
|
570
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-700/80 dark:text-cyan-300/80">
|
|
571
|
+
Course overview
|
|
572
|
+
</div>
|
|
573
|
+
<div className="mt-1 text-sm font-semibold text-foreground">
|
|
574
|
+
Panorama estrutural e de mídia do curso
|
|
575
|
+
</div>
|
|
576
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
577
|
+
Visão consolidada de módulos, aulas, tipos de conteúdo, vídeo e
|
|
578
|
+
apoio.
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
<div className="flex flex-wrap gap-1.5">
|
|
582
|
+
{overviewHighlights.map((item) => (
|
|
583
|
+
<div
|
|
584
|
+
key={item.key}
|
|
585
|
+
className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md ${item.toneClassName}`}
|
|
586
|
+
>
|
|
587
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] opacity-80">
|
|
588
|
+
{item.label}
|
|
589
|
+
</span>
|
|
590
|
+
<span className="font-semibold">{item.value}</span>
|
|
591
|
+
</div>
|
|
592
|
+
))}
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<div className="mt-2">
|
|
597
|
+
<KpiCardsGrid
|
|
598
|
+
items={kpiItems.map((item) => ({
|
|
599
|
+
...item,
|
|
600
|
+
layout: 'compact' as const,
|
|
601
|
+
}))}
|
|
602
|
+
columns={4}
|
|
603
|
+
className="gap-2.5"
|
|
604
|
+
cardClassName="shadow-[0_12px_24px_-22px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_18px_34px_-22px_rgba(15,23,42,0.5)]"
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
610
|
+
<CardHeader className="border-b border-border/70 bg-linear-to-r from-cyan-500/12 via-sky-500/6 to-transparent pt-2.5 pb-1.5!">
|
|
611
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
612
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
613
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-cyan-500/10 text-cyan-600 ring-1 ring-inset ring-cyan-500/15 dark:text-cyan-400">
|
|
614
|
+
<HardDrive className="size-4" />
|
|
615
|
+
</div>
|
|
616
|
+
<div className="min-w-0">
|
|
617
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-cyan-600/80 dark:text-cyan-400/80">
|
|
618
|
+
Storage footprint
|
|
619
|
+
</div>
|
|
620
|
+
<CardTitle className="mt-1 text-sm font-semibold">
|
|
621
|
+
Uso de armazenamento do curso
|
|
622
|
+
</CardTitle>
|
|
623
|
+
<CardDescription className="mt-0.5">
|
|
624
|
+
Soma dos arquivos do conteúdo do curso, com detalhamento por
|
|
625
|
+
categoria.
|
|
626
|
+
</CardDescription>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
<div className="rounded-full border border-cyan-500/15 bg-cyan-500/10 px-2.5 py-1 text-[10px] font-semibold text-cyan-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-cyan-500/15 hover:shadow-md dark:text-cyan-300">
|
|
630
|
+
{formatBytes(data.storage.totalBytes)}
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
</CardHeader>
|
|
634
|
+
<CardContent className="grid gap-3 px-2.5 py-2.5 sm:px-3 xl:grid-cols-[minmax(0,240px)_minmax(0,1fr)]">
|
|
635
|
+
<div className="grid gap-2">
|
|
636
|
+
<div className="rounded-2xl border border-cyan-500/15 bg-linear-to-br from-cyan-500/12 via-sky-500/6 to-transparent px-3 py-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
|
637
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-cyan-700/80 dark:text-cyan-300/80">
|
|
638
|
+
Total armazenado
|
|
639
|
+
</div>
|
|
640
|
+
<div className="mt-2 text-2xl font-semibold tracking-tight text-foreground">
|
|
641
|
+
{formatBytes(data.storage.totalBytes)}
|
|
642
|
+
</div>
|
|
643
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
644
|
+
{data.storage.totalFileCount} arquivos relacionados ao curso
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div className="grid grid-cols-2 gap-2">
|
|
649
|
+
<div className="rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:bg-background/95 hover:shadow-sm">
|
|
650
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
651
|
+
Categorias
|
|
652
|
+
</div>
|
|
653
|
+
<div className="mt-1 text-lg font-semibold text-foreground">
|
|
654
|
+
{storageCategories.length}
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
<div className="rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:bg-background/95 hover:shadow-sm">
|
|
658
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
659
|
+
Maior bloco
|
|
660
|
+
</div>
|
|
661
|
+
<div className="mt-1 truncate text-sm font-semibold text-foreground">
|
|
662
|
+
{storageCategories[0]?.label ?? 'Nenhum'}
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-2">
|
|
669
|
+
{storageCategories.length > 0 ? (
|
|
670
|
+
storageCategories.map((category) => {
|
|
671
|
+
const Icon = category.icon;
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<div
|
|
675
|
+
key={category.key}
|
|
676
|
+
className="overflow-hidden rounded-2xl border border-border/60 bg-card/90 transition-all duration-200 hover:-translate-y-0.5 hover:border-cyan-500/20 hover:bg-background/95 hover:shadow-md"
|
|
677
|
+
>
|
|
678
|
+
<div
|
|
679
|
+
className={`h-1.5 w-full bg-linear-to-r ${category.accentClassName}`}
|
|
680
|
+
/>
|
|
681
|
+
<div className="flex flex-col gap-3 px-3 py-3">
|
|
682
|
+
<div className="flex items-start gap-3">
|
|
683
|
+
<div
|
|
684
|
+
className={`flex size-9 shrink-0 items-center justify-center rounded-xl ${category.iconBackgroundClassName}`}
|
|
685
|
+
>
|
|
686
|
+
<Icon
|
|
687
|
+
className={`size-4 ${category.iconClassName}`}
|
|
688
|
+
/>
|
|
689
|
+
</div>
|
|
690
|
+
<div className="min-w-0 flex-1">
|
|
691
|
+
<div className="text-sm font-semibold text-foreground">
|
|
692
|
+
{category.label}
|
|
693
|
+
</div>
|
|
694
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
695
|
+
{category.fileCount}{' '}
|
|
696
|
+
{category.fileCount === 1
|
|
697
|
+
? 'arquivo'
|
|
698
|
+
: 'arquivos'}
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
<div className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-semibold text-muted-foreground">
|
|
702
|
+
{category.sharePercent.toFixed(
|
|
703
|
+
category.sharePercent >= 10 ? 0 : 1
|
|
704
|
+
)}
|
|
705
|
+
%
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
<div className="flex items-center justify-between gap-2">
|
|
710
|
+
<span className="text-[11px] text-muted-foreground">
|
|
711
|
+
Limpar categoria para liberar espaço
|
|
712
|
+
</span>
|
|
713
|
+
{cleanupTrackedJob?.categoryKey === category.key &&
|
|
714
|
+
[
|
|
715
|
+
'pending',
|
|
716
|
+
'scheduled',
|
|
717
|
+
'processing',
|
|
718
|
+
'retrying',
|
|
719
|
+
].includes(cleanupTrackedJob.status) ? (
|
|
720
|
+
<Badge
|
|
721
|
+
variant="secondary"
|
|
722
|
+
className="h-7 gap-1.5 rounded-lg border border-amber-500/20 bg-amber-500/10 px-2.5 text-[11px] font-semibold text-amber-700 dark:text-amber-300"
|
|
723
|
+
>
|
|
724
|
+
<Loader2 className="size-3 animate-spin" />
|
|
725
|
+
Carregando...
|
|
726
|
+
</Badge>
|
|
727
|
+
) : (
|
|
728
|
+
<Button
|
|
729
|
+
type="button"
|
|
730
|
+
size="sm"
|
|
731
|
+
variant="outline"
|
|
732
|
+
className="h-7 gap-1.5 rounded-lg border-red-500/30 bg-red-500/5 px-2.5 text-[11px] font-semibold text-red-700 transition-all duration-200 hover:-translate-y-0.5 hover:bg-red-500/10 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200"
|
|
733
|
+
disabled={
|
|
734
|
+
cleanupCategoryKey !== null ||
|
|
735
|
+
category.fileCount <= 0
|
|
736
|
+
}
|
|
737
|
+
onClick={() =>
|
|
738
|
+
openCleanupCategoryDialog(
|
|
739
|
+
category.key,
|
|
740
|
+
category.label,
|
|
741
|
+
category.fileCount,
|
|
742
|
+
category.totalBytes
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
>
|
|
746
|
+
{cleanupCategoryKey === category.key ? (
|
|
747
|
+
<>
|
|
748
|
+
<Loader2 className="size-3 animate-spin" />
|
|
749
|
+
Enfileirando...
|
|
750
|
+
</>
|
|
751
|
+
) : (
|
|
752
|
+
<>
|
|
753
|
+
<Trash2 className="size-3" />
|
|
754
|
+
Excluir
|
|
755
|
+
</>
|
|
756
|
+
)}
|
|
757
|
+
</Button>
|
|
758
|
+
)}
|
|
759
|
+
</div>
|
|
760
|
+
|
|
761
|
+
<div>
|
|
762
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
763
|
+
<span className="text-lg font-semibold tracking-tight text-foreground">
|
|
764
|
+
{formatBytes(category.totalBytes)}
|
|
765
|
+
</span>
|
|
766
|
+
<span className="text-xs text-muted-foreground">
|
|
767
|
+
de {formatBytes(data.storage.totalBytes)}
|
|
768
|
+
</span>
|
|
769
|
+
</div>
|
|
770
|
+
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted/80">
|
|
771
|
+
<div
|
|
772
|
+
className="h-full rounded-full transition-all duration-700 ease-out"
|
|
773
|
+
style={{
|
|
774
|
+
width: `${Math.max(category.sharePercent, category.totalBytes > 0 ? 2 : 0)}%`,
|
|
775
|
+
background: `linear-gradient(90deg, rgba(34,211,238,0.9), rgba(14,165,233,0.75))`,
|
|
776
|
+
}}
|
|
777
|
+
/>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
);
|
|
783
|
+
})
|
|
784
|
+
) : (
|
|
785
|
+
<div className="flex min-h-32 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-muted/15 px-4 py-6 text-center text-sm text-muted-foreground">
|
|
786
|
+
Nenhum arquivo relacionado ao curso foi encontrado.
|
|
787
|
+
</div>
|
|
788
|
+
)}
|
|
789
|
+
</div>
|
|
790
|
+
</CardContent>
|
|
791
|
+
</Card>
|
|
792
|
+
|
|
793
|
+
<div className="grid gap-3 xl:grid-cols-2">
|
|
794
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
795
|
+
<CardHeader className="border-b border-border/70 bg-linear-to-r from-sky-500/12 via-blue-500/6 to-transparent pt-2.5 pb-1.5!">
|
|
796
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
797
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
798
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-sky-500/10 text-sky-600 ring-1 ring-inset ring-sky-500/15 dark:text-sky-400">
|
|
799
|
+
<Layers className="size-4" />
|
|
800
|
+
</div>
|
|
801
|
+
<div className="min-w-0">
|
|
802
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-600/80 dark:text-sky-400/80">
|
|
803
|
+
Lesson mix
|
|
804
|
+
</div>
|
|
805
|
+
<CardTitle className="mt-1 text-sm font-semibold">
|
|
806
|
+
{t('lessonTypes.title')}
|
|
807
|
+
</CardTitle>
|
|
808
|
+
<CardDescription className="mt-0.5">
|
|
809
|
+
{t('lessonTypes.description')}
|
|
810
|
+
</CardDescription>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
<div className="rounded-full border border-sky-500/15 bg-sky-500/10 px-2.5 py-1 text-[10px] font-semibold text-sky-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-sky-500/15 hover:shadow-md dark:text-sky-300">
|
|
814
|
+
{totalLessons} aulas
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
</CardHeader>
|
|
818
|
+
<CardContent className="grid gap-2.5 px-2.5 py-2.5 sm:px-3 md:grid-cols-[minmax(0,200px)_minmax(0,1fr)] md:items-start">
|
|
819
|
+
{lessonTypeData.length > 0 ? (
|
|
820
|
+
<ChartContainer
|
|
821
|
+
config={lessonTypeChartConfig}
|
|
822
|
+
className="h-48 w-full"
|
|
823
|
+
>
|
|
824
|
+
<PieChart>
|
|
825
|
+
<Pie
|
|
826
|
+
data={lessonTypeData}
|
|
827
|
+
dataKey="count"
|
|
828
|
+
nameKey="label"
|
|
829
|
+
innerRadius={52}
|
|
830
|
+
outerRadius={80}
|
|
831
|
+
paddingAngle={3}
|
|
832
|
+
animationDuration={900}
|
|
833
|
+
strokeWidth={3}
|
|
834
|
+
stroke="hsl(var(--background))"
|
|
835
|
+
>
|
|
836
|
+
{lessonTypeData.map((entry) => (
|
|
837
|
+
<Cell key={entry.type} fill={entry.color} />
|
|
838
|
+
))}
|
|
839
|
+
</Pie>
|
|
840
|
+
<ChartTooltip
|
|
841
|
+
content={
|
|
842
|
+
<ChartTooltipContent
|
|
843
|
+
hideIndicator
|
|
844
|
+
formatter={(value, _name, item) => {
|
|
845
|
+
const payload = item?.payload as
|
|
846
|
+
| { label?: string; count?: number }
|
|
847
|
+
| undefined;
|
|
848
|
+
const pct =
|
|
849
|
+
totalLessons > 0
|
|
850
|
+
? (
|
|
851
|
+
((payload?.count ?? 0) / totalLessons) *
|
|
852
|
+
100
|
|
853
|
+
).toFixed(1)
|
|
854
|
+
: '0';
|
|
855
|
+
return (
|
|
856
|
+
<div className="flex min-w-36 flex-col gap-1 text-xs">
|
|
857
|
+
<span className="font-medium text-foreground">
|
|
858
|
+
{payload?.label}
|
|
859
|
+
</span>
|
|
860
|
+
<span className="text-muted-foreground">
|
|
861
|
+
{value} aulas · {pct}%
|
|
862
|
+
</span>
|
|
863
|
+
</div>
|
|
864
|
+
);
|
|
865
|
+
}}
|
|
866
|
+
/>
|
|
867
|
+
}
|
|
868
|
+
/>
|
|
869
|
+
</PieChart>
|
|
870
|
+
</ChartContainer>
|
|
871
|
+
) : (
|
|
872
|
+
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
|
|
873
|
+
Nenhuma aula
|
|
874
|
+
</div>
|
|
875
|
+
)}
|
|
876
|
+
|
|
877
|
+
<div className="grid gap-2">
|
|
878
|
+
{lessonTypeData.map((item) => {
|
|
879
|
+
const Icon = TYPE_GRADIENTS[item.type]!.icon;
|
|
880
|
+
const pct =
|
|
881
|
+
totalLessons > 0
|
|
882
|
+
? ((item.count / totalLessons) * 100).toFixed(1)
|
|
883
|
+
: '0';
|
|
884
|
+
return (
|
|
885
|
+
<div
|
|
886
|
+
key={item.type}
|
|
887
|
+
className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-sky-500/20 hover:bg-background/95 hover:shadow-md"
|
|
888
|
+
>
|
|
889
|
+
<div
|
|
890
|
+
className="flex size-8 shrink-0 items-center justify-center rounded-lg"
|
|
891
|
+
style={{ backgroundColor: `${item.color}20` }}
|
|
892
|
+
>
|
|
893
|
+
<Icon
|
|
894
|
+
className="size-4"
|
|
895
|
+
style={{ color: item.color }}
|
|
896
|
+
/>
|
|
897
|
+
</div>
|
|
898
|
+
<div className="min-w-0 flex-1">
|
|
899
|
+
<div className="flex items-center justify-between">
|
|
900
|
+
<span className="text-sm font-medium text-foreground">
|
|
901
|
+
{item.label}
|
|
902
|
+
</span>
|
|
903
|
+
<span className="font-mono text-xs text-muted-foreground transition-colors duration-200 group-hover:text-foreground">
|
|
904
|
+
{pct}%
|
|
905
|
+
</span>
|
|
906
|
+
</div>
|
|
907
|
+
<div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
908
|
+
<div
|
|
909
|
+
className="h-full rounded-full transition-all duration-700"
|
|
910
|
+
style={{
|
|
911
|
+
width: `${pct}%`,
|
|
912
|
+
backgroundColor: item.color,
|
|
913
|
+
}}
|
|
914
|
+
/>
|
|
915
|
+
</div>
|
|
916
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
917
|
+
{item.count} {item.count === 1 ? 'aula' : 'aulas'}
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
);
|
|
922
|
+
})}
|
|
923
|
+
</div>
|
|
924
|
+
</CardContent>
|
|
925
|
+
</Card>
|
|
926
|
+
|
|
927
|
+
{/* Video coverage */}
|
|
928
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
929
|
+
<CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/12 via-cyan-500/6 to-transparent pt-2.5 pb-1.5!">
|
|
930
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
931
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
932
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
|
|
933
|
+
<Clapperboard className="size-4" />
|
|
934
|
+
</div>
|
|
935
|
+
<div className="min-w-0">
|
|
936
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
|
|
937
|
+
Video coverage
|
|
938
|
+
</div>
|
|
939
|
+
<CardTitle className="mt-1 text-sm font-semibold">
|
|
940
|
+
{t('videos.title')}
|
|
941
|
+
</CardTitle>
|
|
942
|
+
<CardDescription className="mt-0.5">
|
|
943
|
+
{t('videos.description')}
|
|
944
|
+
</CardDescription>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
<div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
|
|
948
|
+
{videoCount} vídeos
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
</CardHeader>
|
|
952
|
+
<CardContent className="flex flex-col gap-3 px-2.5 py-2.5 sm:px-3">
|
|
953
|
+
{(
|
|
954
|
+
[
|
|
955
|
+
{
|
|
956
|
+
key: 'total',
|
|
957
|
+
label: t('videos.total'),
|
|
958
|
+
value: videoCount,
|
|
959
|
+
total: videoCount,
|
|
960
|
+
color: '#0ea5e9',
|
|
961
|
+
bgColor: 'bg-sky-500/10',
|
|
962
|
+
textColor: 'text-sky-600 dark:text-sky-400',
|
|
963
|
+
icon: Clapperboard,
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
key: 'transcription',
|
|
967
|
+
label: t('videos.withTranscription'),
|
|
968
|
+
value: data.videos.withTranscription,
|
|
969
|
+
total: videoCount,
|
|
970
|
+
color: '#14b8a6',
|
|
971
|
+
bgColor: 'bg-teal-500/10',
|
|
972
|
+
textColor: 'text-teal-600 dark:text-teal-400',
|
|
973
|
+
icon: Mic,
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
key: 'xp',
|
|
977
|
+
label: t('videos.withXp'),
|
|
978
|
+
value: data.videos.withXp,
|
|
979
|
+
total: videoCount,
|
|
980
|
+
color: '#8b5cf6',
|
|
981
|
+
bgColor: 'bg-violet-500/10',
|
|
982
|
+
textColor: 'text-violet-600 dark:text-violet-400',
|
|
983
|
+
icon: Zap,
|
|
984
|
+
},
|
|
985
|
+
] as const
|
|
986
|
+
).map((stat) => {
|
|
987
|
+
const pct =
|
|
988
|
+
stat.total > 0
|
|
989
|
+
? Math.round((stat.value / stat.total) * 100)
|
|
990
|
+
: stat.key === 'total'
|
|
991
|
+
? 100
|
|
992
|
+
: 0;
|
|
993
|
+
const Icon = stat.icon;
|
|
994
|
+
return (
|
|
995
|
+
<div
|
|
996
|
+
key={stat.key}
|
|
997
|
+
className="flex flex-col gap-2 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 hover:-translate-y-0.5 hover:border-border/60 hover:bg-background/70 hover:shadow-sm"
|
|
998
|
+
>
|
|
999
|
+
<div className="flex items-center gap-3">
|
|
1000
|
+
<div
|
|
1001
|
+
className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${stat.bgColor}`}
|
|
1002
|
+
>
|
|
1003
|
+
<Icon className={`size-4 ${stat.textColor}`} />
|
|
1004
|
+
</div>
|
|
1005
|
+
<div className="flex flex-1 items-center justify-between">
|
|
1006
|
+
<span className="text-sm font-medium text-foreground">
|
|
1007
|
+
{stat.label}
|
|
1008
|
+
</span>
|
|
1009
|
+
<div className="flex items-center gap-2">
|
|
1010
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
1011
|
+
{stat.value}/{stat.total}
|
|
1012
|
+
</span>
|
|
1013
|
+
<span
|
|
1014
|
+
className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
|
|
1015
|
+
style={{
|
|
1016
|
+
backgroundColor: `${stat.color}20`,
|
|
1017
|
+
color: stat.color,
|
|
1018
|
+
}}
|
|
1019
|
+
>
|
|
1020
|
+
{pct}%
|
|
1021
|
+
</span>
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div className="ml-11 h-2 w-full overflow-hidden rounded-full bg-muted">
|
|
1026
|
+
<div
|
|
1027
|
+
className="h-full rounded-full transition-all duration-700 ease-out"
|
|
1028
|
+
style={{
|
|
1029
|
+
width: `${pct}%`,
|
|
1030
|
+
backgroundColor: stat.color,
|
|
1031
|
+
}}
|
|
1032
|
+
/>
|
|
1033
|
+
</div>
|
|
1034
|
+
</div>
|
|
1035
|
+
);
|
|
1036
|
+
})}
|
|
1037
|
+
|
|
1038
|
+
{videoCount === 0 && (
|
|
1039
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
1040
|
+
Nenhuma aula de vídeo neste curso
|
|
1041
|
+
</p>
|
|
1042
|
+
)}
|
|
1043
|
+
</CardContent>
|
|
1044
|
+
</Card>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
{/* Areas and Skills (conditional on XP data) */}
|
|
1048
|
+
{xpData && (xpData.areas.length > 0 || xpData.skills.length > 0) && (
|
|
1049
|
+
<div className="grid gap-3 xl:grid-cols-2">
|
|
1050
|
+
{xpData.areas.length > 0 && (
|
|
1051
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
1052
|
+
<CardHeader className="border-b border-border/70 bg-linear-to-r from-teal-500/10 via-cyan-500/5 to-transparent pt-2.5 pb-1.5!">
|
|
1053
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1054
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
1055
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-teal-500/10 text-teal-600 ring-1 ring-inset ring-teal-500/15 dark:text-teal-400">
|
|
1056
|
+
<Sparkles className="size-4" />
|
|
1057
|
+
</div>
|
|
1058
|
+
<div className="min-w-0">
|
|
1059
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-teal-600/80 dark:text-teal-400/80">
|
|
1060
|
+
Macro areas
|
|
1061
|
+
</div>
|
|
1062
|
+
<CardTitle className="mt-1 text-sm font-semibold">
|
|
1063
|
+
{t('areas.title')}
|
|
1064
|
+
</CardTitle>
|
|
1065
|
+
<CardDescription className="mt-0.5">
|
|
1066
|
+
{t('areas.description')}
|
|
1067
|
+
</CardDescription>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div className="rounded-full border border-teal-500/15 bg-teal-500/10 px-2.5 py-1 text-[10px] font-semibold text-teal-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-teal-500/15 hover:shadow-md dark:text-teal-300">
|
|
1071
|
+
{xpData.areas.length} áreas
|
|
1072
|
+
</div>
|
|
1073
|
+
</div>
|
|
1074
|
+
</CardHeader>
|
|
1075
|
+
<CardContent className="flex flex-col gap-2 px-2.5 py-2.5 sm:px-3">
|
|
1076
|
+
{xpData.areas.map((area, index) => (
|
|
1077
|
+
<div
|
|
1078
|
+
key={area.xpAreaId}
|
|
1079
|
+
className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-teal-500/20 hover:bg-background/95 hover:shadow-md"
|
|
1080
|
+
>
|
|
1081
|
+
<div
|
|
1082
|
+
className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
|
|
1083
|
+
style={{
|
|
1084
|
+
backgroundColor:
|
|
1085
|
+
AREA_COLORS[index % AREA_COLORS.length],
|
|
1086
|
+
}}
|
|
1087
|
+
>
|
|
1088
|
+
{index + 1}
|
|
1089
|
+
</div>
|
|
1090
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
|
|
1091
|
+
{area.name}
|
|
1092
|
+
</span>
|
|
1093
|
+
<span
|
|
1094
|
+
className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
|
|
1095
|
+
style={{
|
|
1096
|
+
backgroundColor: `${AREA_COLORS[index % AREA_COLORS.length]}20`,
|
|
1097
|
+
color: AREA_COLORS[index % AREA_COLORS.length],
|
|
1098
|
+
}}
|
|
1099
|
+
>
|
|
1100
|
+
{area.sharePercent.toFixed(1)}%
|
|
1101
|
+
</span>
|
|
1102
|
+
</div>
|
|
1103
|
+
))}
|
|
1104
|
+
</CardContent>
|
|
1105
|
+
</Card>
|
|
1106
|
+
)}
|
|
1107
|
+
|
|
1108
|
+
{xpData.skills.length > 0 && (
|
|
1109
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
1110
|
+
<CardHeader className="border-b border-border/70 bg-linear-to-r from-orange-500/10 via-amber-500/5 to-transparent pt-2.5 pb-1.5!">
|
|
1111
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1112
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
1113
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-orange-500/10 text-orange-600 ring-1 ring-inset ring-orange-500/15 dark:text-orange-400">
|
|
1114
|
+
<Target className="size-4" />
|
|
1115
|
+
</div>
|
|
1116
|
+
<div className="min-w-0">
|
|
1117
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-orange-600/80 dark:text-orange-400/80">
|
|
1118
|
+
Skills
|
|
1119
|
+
</div>
|
|
1120
|
+
<CardTitle className="mt-1 text-sm font-semibold">
|
|
1121
|
+
{t('skills.title')}
|
|
1122
|
+
</CardTitle>
|
|
1123
|
+
<CardDescription className="mt-0.5">
|
|
1124
|
+
{t('skills.description')}
|
|
1125
|
+
</CardDescription>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
<div className="rounded-full border border-orange-500/15 bg-orange-500/10 px-2.5 py-1 text-[10px] font-semibold text-orange-700 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-orange-500/15 hover:shadow-md dark:text-orange-300">
|
|
1129
|
+
{xpData.skills.length} skills
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
</CardHeader>
|
|
1133
|
+
<CardContent className="flex flex-col gap-2 px-2.5 py-2.5 sm:px-3">
|
|
1134
|
+
{xpData.skills.map((skill, index) => (
|
|
1135
|
+
<div
|
|
1136
|
+
key={skill.xpSkillId}
|
|
1137
|
+
className="flex items-center gap-3 rounded-xl border border-border/60 bg-muted/20 px-3 py-2.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-orange-500/20 hover:bg-background/95 hover:shadow-md"
|
|
1138
|
+
>
|
|
1139
|
+
<div
|
|
1140
|
+
className="flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-bold text-white"
|
|
1141
|
+
style={{
|
|
1142
|
+
backgroundColor:
|
|
1143
|
+
SKILL_COLORS[index % SKILL_COLORS.length],
|
|
1144
|
+
}}
|
|
1145
|
+
>
|
|
1146
|
+
{index + 1}
|
|
1147
|
+
</div>
|
|
1148
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
|
|
1149
|
+
{skill.name}
|
|
1150
|
+
</span>
|
|
1151
|
+
<span
|
|
1152
|
+
className="rounded-full px-2 py-0.5 text-[10px] font-semibold transition-all duration-200 hover:shadow-sm"
|
|
1153
|
+
style={{
|
|
1154
|
+
backgroundColor: `${SKILL_COLORS[index % SKILL_COLORS.length]}20`,
|
|
1155
|
+
color: SKILL_COLORS[index % SKILL_COLORS.length],
|
|
1156
|
+
}}
|
|
1157
|
+
>
|
|
1158
|
+
{skill.sharePercent.toFixed(1)}%
|
|
1159
|
+
</span>
|
|
1160
|
+
</div>
|
|
1161
|
+
))}
|
|
1162
|
+
</CardContent>
|
|
1163
|
+
</Card>
|
|
1164
|
+
)}
|
|
1165
|
+
</div>
|
|
1166
|
+
)}
|
|
1167
|
+
</div>
|
|
1168
|
+
</>
|
|
1169
|
+
);
|
|
1170
|
+
}
|