@hed-hog/lms 0.0.358 → 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 +15 -3
- 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 +153 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.controller.js +129 -0
- package/dist/course/lms-bulk-upload.controller.js.map +1 -0
- package/dist/course/lms-bulk-upload.service.d.ts +181 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.service.js +754 -0
- package/dist/course/lms-bulk-upload.service.js.map +1 -0
- package/dist/course/lms-setting.controller.d.ts +7 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +26 -3
- 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 +94 -7
- 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/role.yaml +8 -0
- package/hedhog/data/route.yaml +547 -0
- package/hedhog/data/setting_group.yaml +33 -0
- 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 +9 -8
- 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 +15 -3
- 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 +103 -0
- package/src/course/lms-bulk-upload.service.ts +1029 -0
- package/src/course/lms-setting.controller.ts +28 -1
- 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,1453 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CopyButton } from '@/components/copy-button';
|
|
4
|
+
import {
|
|
5
|
+
EmptyState,
|
|
6
|
+
Page,
|
|
7
|
+
PageHeader,
|
|
8
|
+
PaginationFooter,
|
|
9
|
+
SearchBar,
|
|
10
|
+
} from '@/components/entity-list';
|
|
11
|
+
import { FileTypeIcon } from '@/components/file-type-icon';
|
|
12
|
+
import {
|
|
13
|
+
IntegrationProfileSheet,
|
|
14
|
+
type IntegrationProfileSheetSavedProfile,
|
|
15
|
+
} from '@/components/integration-profile/integration-profile-sheet';
|
|
16
|
+
import {
|
|
17
|
+
AlertDialog,
|
|
18
|
+
AlertDialogAction,
|
|
19
|
+
AlertDialogCancel,
|
|
20
|
+
AlertDialogContent,
|
|
21
|
+
AlertDialogDescription,
|
|
22
|
+
AlertDialogHeader,
|
|
23
|
+
AlertDialogTitle,
|
|
24
|
+
} from '@/components/ui/alert-dialog';
|
|
25
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
26
|
+
import { Badge } from '@/components/ui/badge';
|
|
27
|
+
import { Button } from '@/components/ui/button';
|
|
28
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
29
|
+
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
30
|
+
import { Input } from '@/components/ui/input';
|
|
31
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
32
|
+
import { Label } from '@/components/ui/label';
|
|
33
|
+
import { Progress } from '@/components/ui/progress';
|
|
34
|
+
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
35
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
36
|
+
import {
|
|
37
|
+
Sheet,
|
|
38
|
+
SheetDescription,
|
|
39
|
+
SheetHeader,
|
|
40
|
+
SheetTitle,
|
|
41
|
+
} from '@/components/ui/sheet';
|
|
42
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
43
|
+
import {
|
|
44
|
+
Table,
|
|
45
|
+
TableBody,
|
|
46
|
+
TableCell,
|
|
47
|
+
TableHead,
|
|
48
|
+
TableHeader,
|
|
49
|
+
TableRow,
|
|
50
|
+
} from '@/components/ui/table';
|
|
51
|
+
import {
|
|
52
|
+
Tooltip,
|
|
53
|
+
TooltipContent,
|
|
54
|
+
TooltipProvider,
|
|
55
|
+
TooltipTrigger,
|
|
56
|
+
} from '@/components/ui/tooltip';
|
|
57
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
58
|
+
import { getPhotoUrl } from '@/lib/get-photo-url';
|
|
59
|
+
import { cn } from '@/lib/utils';
|
|
60
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
61
|
+
import {
|
|
62
|
+
Ban,
|
|
63
|
+
CheckCircle2,
|
|
64
|
+
Clock,
|
|
65
|
+
Cog,
|
|
66
|
+
ExternalLink,
|
|
67
|
+
HardDriveUpload,
|
|
68
|
+
Inbox,
|
|
69
|
+
Loader2,
|
|
70
|
+
Pencil,
|
|
71
|
+
Plus,
|
|
72
|
+
RefreshCw,
|
|
73
|
+
Save,
|
|
74
|
+
Trash2,
|
|
75
|
+
Webhook,
|
|
76
|
+
XCircle,
|
|
77
|
+
} from 'lucide-react';
|
|
78
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
79
|
+
import { toast } from 'sonner';
|
|
80
|
+
|
|
81
|
+
type UploadStatus =
|
|
82
|
+
| 'queued'
|
|
83
|
+
| 'uploading'
|
|
84
|
+
| 'cancelling'
|
|
85
|
+
| 'received'
|
|
86
|
+
| 'done'
|
|
87
|
+
| 'error'
|
|
88
|
+
| 'cancelled';
|
|
89
|
+
|
|
90
|
+
type UploadItemRow = {
|
|
91
|
+
id: number;
|
|
92
|
+
sessionId: number;
|
|
93
|
+
uploadId: string;
|
|
94
|
+
fileName: string;
|
|
95
|
+
sizeBytes: number;
|
|
96
|
+
status: UploadStatus;
|
|
97
|
+
progressPercent: number;
|
|
98
|
+
errorMessage: string | null;
|
|
99
|
+
uploadedKey: string | null;
|
|
100
|
+
receivedAt: string | null;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
completedAt: string | null;
|
|
103
|
+
appName: string;
|
|
104
|
+
userId: number;
|
|
105
|
+
userPhotoId: number | null;
|
|
106
|
+
userName: string | null;
|
|
107
|
+
sessionStatus: string;
|
|
108
|
+
startedAt: string;
|
|
109
|
+
matchedCourseId: number | null;
|
|
110
|
+
matchedCourseTitle: string | null;
|
|
111
|
+
matchedCourseSlug: string | null;
|
|
112
|
+
matchedCourseLogoFileId: number | null;
|
|
113
|
+
matchedSessionId: number | null;
|
|
114
|
+
matchedSessionTitle: string | null;
|
|
115
|
+
matchedLessonId: number | null;
|
|
116
|
+
matchedLessonTitle: string | null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type UploadListResponse = {
|
|
120
|
+
data: UploadItemRow[];
|
|
121
|
+
total: number;
|
|
122
|
+
page: number;
|
|
123
|
+
pageSize: number;
|
|
124
|
+
lastPage: number;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type Paginated<T> = {
|
|
128
|
+
data: T[];
|
|
129
|
+
total: number;
|
|
130
|
+
page: number;
|
|
131
|
+
pageSize: number;
|
|
132
|
+
lastPage: number;
|
|
133
|
+
prev?: number | null;
|
|
134
|
+
next?: number | null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type BulkUploadSettingsResponse = {
|
|
138
|
+
storageProfileId: number | null;
|
|
139
|
+
bucketName: string;
|
|
140
|
+
lambdaRoleArn: string;
|
|
141
|
+
sessionDurationSeconds: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
type IntegrationProfileOption = {
|
|
145
|
+
id: number;
|
|
146
|
+
name: string;
|
|
147
|
+
slug: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type WebhookIntegrationItem = {
|
|
151
|
+
id: number;
|
|
152
|
+
slug: string;
|
|
153
|
+
name: string;
|
|
154
|
+
status: 'active' | 'inactive';
|
|
155
|
+
require_token: boolean;
|
|
156
|
+
public_url: string | null;
|
|
157
|
+
plainToken?: string | null;
|
|
158
|
+
updated_at?: string | null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
type BulkUploadInfrastructureResponse = {
|
|
162
|
+
outputs?: {
|
|
163
|
+
bucket_arn?: string;
|
|
164
|
+
bucket_name?: string;
|
|
165
|
+
lambda_arn?: string;
|
|
166
|
+
lambda_function_name?: string;
|
|
167
|
+
} | null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
type BulkUploadConfigureResponse = {
|
|
171
|
+
success: boolean;
|
|
172
|
+
storageProfileId: number;
|
|
173
|
+
bucketName: string;
|
|
174
|
+
region: string;
|
|
175
|
+
credentials: {
|
|
176
|
+
expiresAt: string | null;
|
|
177
|
+
};
|
|
178
|
+
infrastructure?: BulkUploadInfrastructureResponse;
|
|
179
|
+
webhook: WebhookIntegrationItem;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
type BulkUploadRegenerateTokenResponse = {
|
|
183
|
+
success: boolean;
|
|
184
|
+
storageProfileId: number;
|
|
185
|
+
bucketName: string;
|
|
186
|
+
region: string;
|
|
187
|
+
infrastructure?: BulkUploadInfrastructureResponse;
|
|
188
|
+
webhook: WebhookIntegrationItem;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
type BulkUploadCleanupStatus = 'done' | 'error' | 'cancelled';
|
|
192
|
+
type BulkUploadCleanupTimeWindow =
|
|
193
|
+
| 'last_hour'
|
|
194
|
+
| 'last_day'
|
|
195
|
+
| 'last_week'
|
|
196
|
+
| 'all_time';
|
|
197
|
+
|
|
198
|
+
type BulkUploadCleanupResponse = {
|
|
199
|
+
success: boolean;
|
|
200
|
+
deletedItems: number;
|
|
201
|
+
deletedSessions: number;
|
|
202
|
+
filtersApplied: {
|
|
203
|
+
statuses: BulkUploadCleanupStatus[];
|
|
204
|
+
timeWindow: BulkUploadCleanupTimeWindow;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const BULK_UPLOAD_STORAGE_PROFILE_SLUG = 'lms-bulk-upload-storage-profile-id';
|
|
209
|
+
const BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG = 'lms-bulk-upload-lambda-role-arn';
|
|
210
|
+
const BULK_UPLOAD_WEBHOOK_SLUG = 'lms-bulk-upload';
|
|
211
|
+
|
|
212
|
+
const PAGE_SIZE_OPTIONS = [6, 12, 24, 48] as const;
|
|
213
|
+
const CLEANUP_STATUS_OPTIONS: Array<{
|
|
214
|
+
value: BulkUploadCleanupStatus;
|
|
215
|
+
label: string;
|
|
216
|
+
}> = [
|
|
217
|
+
{ value: 'done', label: 'Concluidos' },
|
|
218
|
+
{ value: 'error', label: 'Com falha' },
|
|
219
|
+
{ value: 'cancelled', label: 'Cancelados' },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const CLEANUP_TIME_WINDOW_OPTIONS: Array<{
|
|
223
|
+
value: BulkUploadCleanupTimeWindow;
|
|
224
|
+
label: string;
|
|
225
|
+
description: string;
|
|
226
|
+
}> = [
|
|
227
|
+
{
|
|
228
|
+
value: 'last_hour',
|
|
229
|
+
label: 'Ultima hora',
|
|
230
|
+
description: 'Remove itens atualizados na ultima hora.',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
value: 'last_day',
|
|
234
|
+
label: 'Ultimo dia',
|
|
235
|
+
description: 'Remove itens atualizados nas ultimas 24 horas.',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
value: 'last_week',
|
|
239
|
+
label: 'Ultima semana',
|
|
240
|
+
description: 'Remove itens atualizados nos ultimos 7 dias.',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
value: 'all_time',
|
|
244
|
+
label: 'Todos de sempre',
|
|
245
|
+
description: 'Remove todo o historico conforme os status marcados.',
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
function formatBytes(bytes: number) {
|
|
250
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
251
|
+
|
|
252
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
253
|
+
const exponent = Math.min(
|
|
254
|
+
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
255
|
+
units.length - 1
|
|
256
|
+
);
|
|
257
|
+
const value = bytes / 1024 ** exponent;
|
|
258
|
+
|
|
259
|
+
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[exponent]}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatDate(value: string | null) {
|
|
263
|
+
if (!value) return '-';
|
|
264
|
+
const date = new Date(value);
|
|
265
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
266
|
+
return date.toLocaleString('pt-BR');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getStatusMeta(status: UploadStatus) {
|
|
270
|
+
switch (status) {
|
|
271
|
+
case 'received':
|
|
272
|
+
return {
|
|
273
|
+
label: 'Arquivo recebido',
|
|
274
|
+
variant: 'default' as const,
|
|
275
|
+
icon: Inbox,
|
|
276
|
+
iconClass: 'text-green-600',
|
|
277
|
+
animated: false,
|
|
278
|
+
progressClass: 'bg-green-500',
|
|
279
|
+
};
|
|
280
|
+
case 'done':
|
|
281
|
+
return {
|
|
282
|
+
label: 'Concluido',
|
|
283
|
+
variant: 'default' as const,
|
|
284
|
+
icon: CheckCircle2,
|
|
285
|
+
iconClass: 'text-green-600',
|
|
286
|
+
animated: false,
|
|
287
|
+
progressClass: 'bg-green-500',
|
|
288
|
+
};
|
|
289
|
+
case 'uploading':
|
|
290
|
+
return {
|
|
291
|
+
label: 'Enviando',
|
|
292
|
+
variant: 'secondary' as const,
|
|
293
|
+
icon: Loader2,
|
|
294
|
+
iconClass: 'text-amber-500 animate-spin',
|
|
295
|
+
animated: true,
|
|
296
|
+
progressClass: 'bg-amber-500',
|
|
297
|
+
};
|
|
298
|
+
case 'queued':
|
|
299
|
+
return {
|
|
300
|
+
label: 'Na fila',
|
|
301
|
+
variant: 'outline' as const,
|
|
302
|
+
icon: Clock,
|
|
303
|
+
iconClass: 'text-blue-500 animate-pulse',
|
|
304
|
+
animated: true,
|
|
305
|
+
progressClass: 'bg-blue-400',
|
|
306
|
+
};
|
|
307
|
+
case 'cancelling':
|
|
308
|
+
return {
|
|
309
|
+
label: 'Cancelando',
|
|
310
|
+
variant: 'secondary' as const,
|
|
311
|
+
icon: Loader2,
|
|
312
|
+
iconClass: 'text-orange-500 animate-spin',
|
|
313
|
+
animated: true,
|
|
314
|
+
progressClass: 'bg-orange-400',
|
|
315
|
+
};
|
|
316
|
+
case 'cancelled':
|
|
317
|
+
return {
|
|
318
|
+
label: 'Cancelado',
|
|
319
|
+
variant: 'outline' as const,
|
|
320
|
+
icon: Ban,
|
|
321
|
+
iconClass: 'text-muted-foreground',
|
|
322
|
+
animated: false,
|
|
323
|
+
progressClass: 'bg-muted-foreground/40',
|
|
324
|
+
};
|
|
325
|
+
default:
|
|
326
|
+
return {
|
|
327
|
+
label: 'Erro',
|
|
328
|
+
variant: 'destructive' as const,
|
|
329
|
+
icon: XCircle,
|
|
330
|
+
iconClass: 'text-destructive animate-pulse',
|
|
331
|
+
animated: true,
|
|
332
|
+
progressClass: 'bg-destructive',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export default function LmsBulkUploadSessionsPage() {
|
|
338
|
+
const { request } = useApp();
|
|
339
|
+
const [page, setPage] = useState(1);
|
|
340
|
+
const [searchInput, setSearchInput] = useState('');
|
|
341
|
+
const [search, setSearch] = useState('');
|
|
342
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
343
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
344
|
+
const [bucketName, setBucketName] = useState('');
|
|
345
|
+
const [lambdaRoleArn, setLambdaRoleArn] = useState('');
|
|
346
|
+
const [storageProfileId, setStorageProfileId] = useState<number | null>(null);
|
|
347
|
+
const [webhookPlainToken, setWebhookPlainToken] = useState<string | null>(
|
|
348
|
+
null
|
|
349
|
+
);
|
|
350
|
+
const [webhookPreview, setWebhookPreview] =
|
|
351
|
+
useState<WebhookIntegrationItem | null>(null);
|
|
352
|
+
const [isSavingSettings, setIsSavingSettings] = useState(false);
|
|
353
|
+
const [isRegeneratingToken, setIsRegeneratingToken] = useState(false);
|
|
354
|
+
const [lastSetupFeedback, setLastSetupFeedback] = useState<{
|
|
355
|
+
lambdaName: string;
|
|
356
|
+
configuredAt: string;
|
|
357
|
+
} | null>(null);
|
|
358
|
+
const [profileRefreshToken, setProfileRefreshToken] = useState(0);
|
|
359
|
+
const [isProfileSheetOpen, setIsProfileSheetOpen] = useState(false);
|
|
360
|
+
const [editingProfileId, setEditingProfileId] = useState<number | null>(null);
|
|
361
|
+
const [isCleanupDialogOpen, setIsCleanupDialogOpen] = useState(false);
|
|
362
|
+
const [cleanupStatuses, setCleanupStatuses] = useState<
|
|
363
|
+
Set<BulkUploadCleanupStatus>
|
|
364
|
+
>(new Set(['done']));
|
|
365
|
+
const [cleanupTimeWindow, setCleanupTimeWindow] =
|
|
366
|
+
useState<BulkUploadCleanupTimeWindow>('last_day');
|
|
367
|
+
const [isCleaningHistory, setIsCleaningHistory] = useState(false);
|
|
368
|
+
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
369
|
+
storageKey: 'pagination:global:pageSize',
|
|
370
|
+
defaultValue: PAGE_SIZE_OPTIONS[1],
|
|
371
|
+
allowedValues: PAGE_SIZE_OPTIONS,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const {
|
|
375
|
+
data: settingsResult,
|
|
376
|
+
isLoading: settingsLoading,
|
|
377
|
+
refetch: refetchSettings,
|
|
378
|
+
} = useQuery<BulkUploadSettingsResponse>({
|
|
379
|
+
queryKey: ['lms-bulk-upload-settings', settingsOpen],
|
|
380
|
+
enabled: settingsOpen,
|
|
381
|
+
queryFn: async () => {
|
|
382
|
+
const response = await request<BulkUploadSettingsResponse>({
|
|
383
|
+
url: '/lms/bulk-upload/settings',
|
|
384
|
+
method: 'GET',
|
|
385
|
+
});
|
|
386
|
+
return response.data;
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const {
|
|
391
|
+
data: profilesResult,
|
|
392
|
+
isLoading: profilesLoading,
|
|
393
|
+
refetch: refetchProfiles,
|
|
394
|
+
} = useQuery<Paginated<IntegrationProfileOption>>({
|
|
395
|
+
queryKey: ['integration-storage-profiles', profileRefreshToken],
|
|
396
|
+
enabled: settingsOpen,
|
|
397
|
+
queryFn: async () => {
|
|
398
|
+
const response = await request<Paginated<IntegrationProfileOption>>({
|
|
399
|
+
url: '/integration-profile?typeSlug=storage&pageSize=100',
|
|
400
|
+
method: 'GET',
|
|
401
|
+
});
|
|
402
|
+
return response.data;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const { data: webhooksResult, refetch: refetchWebhooks } = useQuery<
|
|
407
|
+
Paginated<WebhookIntegrationItem>
|
|
408
|
+
>({
|
|
409
|
+
queryKey: ['lms-bulk-upload-webhook', settingsOpen],
|
|
410
|
+
enabled: settingsOpen,
|
|
411
|
+
queryFn: async () => {
|
|
412
|
+
const response = await request<Paginated<WebhookIntegrationItem>>({
|
|
413
|
+
url: `/webhook-integration?page=1&pageSize=100&search=${BULK_UPLOAD_WEBHOOK_SLUG}`,
|
|
414
|
+
method: 'GET',
|
|
415
|
+
});
|
|
416
|
+
return response.data;
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const {
|
|
421
|
+
data,
|
|
422
|
+
isLoading,
|
|
423
|
+
refetch: refetchSessions,
|
|
424
|
+
} = useQuery<UploadListResponse>({
|
|
425
|
+
queryKey: [
|
|
426
|
+
'lms-bulk-upload-sessions',
|
|
427
|
+
page,
|
|
428
|
+
pageSize,
|
|
429
|
+
search,
|
|
430
|
+
statusFilter,
|
|
431
|
+
],
|
|
432
|
+
queryFn: async () => {
|
|
433
|
+
const response = await request<UploadListResponse>({
|
|
434
|
+
url: '/lms/bulk-upload/sessions',
|
|
435
|
+
method: 'GET',
|
|
436
|
+
params: {
|
|
437
|
+
page,
|
|
438
|
+
pageSize,
|
|
439
|
+
search: search.trim() || undefined,
|
|
440
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
return response.data;
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const rows = useMemo(() => data?.data ?? [], [data?.data]);
|
|
448
|
+
const total = data?.total ?? 0;
|
|
449
|
+
const stats = useMemo(
|
|
450
|
+
() => ({
|
|
451
|
+
sending: rows.filter((row) => row.status === 'uploading').length,
|
|
452
|
+
done: rows.filter((row) => row.status === 'done').length,
|
|
453
|
+
error: rows.filter((row) => row.status === 'error').length,
|
|
454
|
+
}),
|
|
455
|
+
[rows]
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const kpiItems = useMemo(
|
|
459
|
+
() => [
|
|
460
|
+
{
|
|
461
|
+
key: 'sending',
|
|
462
|
+
title: 'Enviando',
|
|
463
|
+
value: isLoading ? '-' : stats.sending,
|
|
464
|
+
icon: Loader2,
|
|
465
|
+
accentClassName: 'from-amber-500/20 via-amber-500/10 to-transparent',
|
|
466
|
+
iconContainerClassName: 'bg-amber-500/10 text-amber-700',
|
|
467
|
+
layout: 'compact' as const,
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
key: 'done',
|
|
471
|
+
title: 'Concluidos',
|
|
472
|
+
value: isLoading ? '-' : stats.done,
|
|
473
|
+
icon: CheckCircle2,
|
|
474
|
+
accentClassName: 'from-green-500/20 via-green-500/10 to-transparent',
|
|
475
|
+
iconContainerClassName: 'bg-green-500/10 text-green-700',
|
|
476
|
+
layout: 'compact' as const,
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
key: 'error',
|
|
480
|
+
title: 'Erros',
|
|
481
|
+
value: isLoading ? '-' : stats.error,
|
|
482
|
+
icon: XCircle,
|
|
483
|
+
accentClassName: 'from-red-500/20 via-red-500/10 to-transparent',
|
|
484
|
+
iconContainerClassName: 'bg-red-500/10 text-red-700',
|
|
485
|
+
layout: 'compact' as const,
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
[isLoading, stats]
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const storageProfileOptions = useMemo(
|
|
492
|
+
() => profilesResult?.data ?? [],
|
|
493
|
+
[profilesResult?.data]
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const selectedStorageProfile = useMemo(
|
|
497
|
+
() =>
|
|
498
|
+
storageProfileOptions.find(
|
|
499
|
+
(profile) => profile.id === storageProfileId
|
|
500
|
+
) ?? null,
|
|
501
|
+
[storageProfileId, storageProfileOptions]
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const webhookIntegration = useMemo(
|
|
505
|
+
() =>
|
|
506
|
+
(webhooksResult?.data ?? []).find(
|
|
507
|
+
(item) =>
|
|
508
|
+
item.slug === BULK_UPLOAD_WEBHOOK_SLUG ||
|
|
509
|
+
item.slug.startsWith(`${BULK_UPLOAD_WEBHOOK_SLUG}-`)
|
|
510
|
+
) ??
|
|
511
|
+
webhookPreview ??
|
|
512
|
+
null,
|
|
513
|
+
[webhookPreview, webhooksResult?.data]
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const hasBaseConfig = Boolean(storageProfileId && lambdaRoleArn.trim());
|
|
517
|
+
const isWebhookActive = Boolean(
|
|
518
|
+
webhookIntegration?.status === 'active' && webhookIntegration?.public_url
|
|
519
|
+
);
|
|
520
|
+
const isFullyConfigured =
|
|
521
|
+
Boolean(lastSetupFeedback) || (hasBaseConfig && isWebhookActive);
|
|
522
|
+
|
|
523
|
+
const currentStatusLabel =
|
|
524
|
+
isSavingSettings || isRegeneratingToken
|
|
525
|
+
? 'Validando e sincronizando...'
|
|
526
|
+
: isFullyConfigured
|
|
527
|
+
? 'Configurado e funcionando'
|
|
528
|
+
: hasBaseConfig
|
|
529
|
+
? 'Pronto para sincronizar'
|
|
530
|
+
: 'Configuração incompleta';
|
|
531
|
+
|
|
532
|
+
const currentStatusDescription =
|
|
533
|
+
isSavingSettings || isRegeneratingToken
|
|
534
|
+
? 'Aguarde a conclusão do provisionamento da integração.'
|
|
535
|
+
: isFullyConfigured
|
|
536
|
+
? 'Bucket, webhook e Lambda estão ativos e alinhados.'
|
|
537
|
+
: hasBaseConfig
|
|
538
|
+
? 'Falta validar/sincronizar para concluir a operação.'
|
|
539
|
+
: 'Selecione o perfil de integração para iniciar a configuração.';
|
|
540
|
+
|
|
541
|
+
useEffect(() => {
|
|
542
|
+
setBucketName(String(settingsResult?.bucketName ?? '').trim());
|
|
543
|
+
setLambdaRoleArn(String(settingsResult?.lambdaRoleArn ?? '').trim());
|
|
544
|
+
const parsedProfileId = Number(settingsResult?.storageProfileId ?? 0);
|
|
545
|
+
setStorageProfileId(
|
|
546
|
+
Number.isFinite(parsedProfileId) && parsedProfileId > 0
|
|
547
|
+
? parsedProfileId
|
|
548
|
+
: null
|
|
549
|
+
);
|
|
550
|
+
}, [
|
|
551
|
+
settingsResult?.bucketName,
|
|
552
|
+
settingsResult?.lambdaRoleArn,
|
|
553
|
+
settingsResult?.storageProfileId,
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
const saveBulkUploadSettings = async () => {
|
|
557
|
+
if (!storageProfileId || !lambdaRoleArn.trim()) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
setIsSavingSettings(true);
|
|
563
|
+
const response = await request<BulkUploadConfigureResponse>({
|
|
564
|
+
url: '/lms/bulk-upload/verify',
|
|
565
|
+
method: 'POST',
|
|
566
|
+
data: {
|
|
567
|
+
configure: true,
|
|
568
|
+
storageProfileId,
|
|
569
|
+
lambdaRoleArn: lambdaRoleArn.trim(),
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
setWebhookPreview(response.data.webhook ?? null);
|
|
574
|
+
setWebhookPlainToken(response.data.webhook?.plainToken ?? null);
|
|
575
|
+
|
|
576
|
+
const lambdaName =
|
|
577
|
+
response.data.infrastructure?.outputs?.lambda_function_name?.trim() ||
|
|
578
|
+
'Lambda não identificada';
|
|
579
|
+
|
|
580
|
+
const configuredAtRaw =
|
|
581
|
+
response.data.webhook?.updated_at ?? new Date().toISOString();
|
|
582
|
+
const configuredAt = formatDate(configuredAtRaw);
|
|
583
|
+
|
|
584
|
+
setLastSetupFeedback({
|
|
585
|
+
lambdaName,
|
|
586
|
+
configuredAt,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
toast.success('Tudo pronto!', {
|
|
590
|
+
description: `Lambda: ${lambdaName} • Configurado em: ${configuredAt}`,
|
|
591
|
+
});
|
|
592
|
+
await Promise.all([
|
|
593
|
+
refetchSettings(),
|
|
594
|
+
refetchProfiles(),
|
|
595
|
+
refetchWebhooks(),
|
|
596
|
+
]);
|
|
597
|
+
} catch {
|
|
598
|
+
toast.error(
|
|
599
|
+
'Falha ao validar bucket/credenciais temporárias ou salvar a configuração.'
|
|
600
|
+
);
|
|
601
|
+
} finally {
|
|
602
|
+
setIsSavingSettings(false);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const regenerateWebhookToken = async () => {
|
|
607
|
+
if (!webhookIntegration) return;
|
|
608
|
+
try {
|
|
609
|
+
setIsRegeneratingToken(true);
|
|
610
|
+
const response = await request<BulkUploadRegenerateTokenResponse>({
|
|
611
|
+
url: '/lms/bulk-upload/webhook/regenerate-token',
|
|
612
|
+
method: 'POST',
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
setWebhookPreview(response.data.webhook ?? null);
|
|
616
|
+
setWebhookPlainToken(response.data.webhook?.plainToken ?? null);
|
|
617
|
+
|
|
618
|
+
const lambdaName =
|
|
619
|
+
response.data.infrastructure?.outputs?.lambda_function_name?.trim() ||
|
|
620
|
+
'Lambda não identificada';
|
|
621
|
+
|
|
622
|
+
const configuredAtRaw =
|
|
623
|
+
response.data.webhook?.updated_at ?? new Date().toISOString();
|
|
624
|
+
const configuredAt = formatDate(configuredAtRaw);
|
|
625
|
+
|
|
626
|
+
setLastSetupFeedback({
|
|
627
|
+
lambdaName,
|
|
628
|
+
configuredAt,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
toast.success('Token regenerado e infraestrutura sincronizada.', {
|
|
632
|
+
description: `Lambda: ${lambdaName} • Configurado em: ${configuredAt}`,
|
|
633
|
+
});
|
|
634
|
+
await refetchWebhooks();
|
|
635
|
+
} catch {
|
|
636
|
+
toast.error('Não foi possível regenerar o token e sincronizar a Lambda.');
|
|
637
|
+
} finally {
|
|
638
|
+
setIsRegeneratingToken(false);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const openCreateProfileSheet = () => {
|
|
643
|
+
setEditingProfileId(null);
|
|
644
|
+
setIsProfileSheetOpen(true);
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const openEditProfileSheet = () => {
|
|
648
|
+
if (!selectedStorageProfile) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
setEditingProfileId(selectedStorageProfile.id);
|
|
653
|
+
setIsProfileSheetOpen(true);
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const handleProfileSaved = (profile: IntegrationProfileSheetSavedProfile) => {
|
|
657
|
+
setStorageProfileId(profile.id);
|
|
658
|
+
setProfileRefreshToken((current) => current + 1);
|
|
659
|
+
refetchProfiles();
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const toggleCleanupStatus = (status: BulkUploadCleanupStatus) => {
|
|
663
|
+
setCleanupStatuses((current) => {
|
|
664
|
+
const next = new Set(current);
|
|
665
|
+
if (next.has(status)) {
|
|
666
|
+
next.delete(status);
|
|
667
|
+
} else {
|
|
668
|
+
next.add(status);
|
|
669
|
+
}
|
|
670
|
+
return next;
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const cleanupStatusSelection = useMemo(
|
|
675
|
+
() => Array.from(cleanupStatuses),
|
|
676
|
+
[cleanupStatuses]
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const runCleanupHistory = async () => {
|
|
680
|
+
if (cleanupStatusSelection.length === 0) {
|
|
681
|
+
toast.error('Selecione ao menos um status para limpar.');
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
setIsCleaningHistory(true);
|
|
687
|
+
const response = await request<BulkUploadCleanupResponse>({
|
|
688
|
+
url: '/lms/bulk-upload/sessions/cleanup',
|
|
689
|
+
method: 'POST',
|
|
690
|
+
data: {
|
|
691
|
+
statuses: cleanupStatusSelection,
|
|
692
|
+
timeWindow: cleanupTimeWindow,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const deletedItems = Number(response.data?.deletedItems ?? 0);
|
|
697
|
+
const deletedSessions = Number(response.data?.deletedSessions ?? 0);
|
|
698
|
+
|
|
699
|
+
toast.success('Historico limpo com sucesso.', {
|
|
700
|
+
description: `${deletedItems} upload(s) e ${deletedSessions} sessao(oes) removidos.`,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
setIsCleanupDialogOpen(false);
|
|
704
|
+
await refetchSessions();
|
|
705
|
+
} catch {
|
|
706
|
+
toast.error('Nao foi possivel limpar o historico de uploads.');
|
|
707
|
+
} finally {
|
|
708
|
+
setIsCleaningHistory(false);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
return (
|
|
713
|
+
<Page>
|
|
714
|
+
<PageHeader
|
|
715
|
+
title="Uploads Desktop"
|
|
716
|
+
description="Monitore arquivos enviados via Hedhog Desktop com status, progresso e usuario responsavel."
|
|
717
|
+
breadcrumbs={[
|
|
718
|
+
{ label: 'Inicio', href: '/' },
|
|
719
|
+
{ label: 'LMS', href: '/lms' },
|
|
720
|
+
{ label: 'Uploads Desktop' },
|
|
721
|
+
]}
|
|
722
|
+
actions={[
|
|
723
|
+
{
|
|
724
|
+
label: 'Limpar historico',
|
|
725
|
+
ariaLabel: 'Limpar historico',
|
|
726
|
+
iconOnly: true,
|
|
727
|
+
variant: 'outline',
|
|
728
|
+
icon: <Trash2 className="h-4 w-4" />,
|
|
729
|
+
onClick: () => setIsCleanupDialogOpen(true),
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
label: 'Configurações',
|
|
733
|
+
ariaLabel: 'Configurações',
|
|
734
|
+
iconOnly: true,
|
|
735
|
+
variant: 'outline',
|
|
736
|
+
icon: <Cog className="h-4 w-4" />,
|
|
737
|
+
onClick: () => setSettingsOpen(true),
|
|
738
|
+
},
|
|
739
|
+
]}
|
|
740
|
+
/>
|
|
741
|
+
|
|
742
|
+
<Sheet
|
|
743
|
+
open={settingsOpen}
|
|
744
|
+
onOpenChange={(open) => {
|
|
745
|
+
setSettingsOpen(open);
|
|
746
|
+
if (!open) {
|
|
747
|
+
setWebhookPlainToken(null);
|
|
748
|
+
}
|
|
749
|
+
}}
|
|
750
|
+
>
|
|
751
|
+
<ResizableSheetContent
|
|
752
|
+
sheetId="lms-bulk-upload-settings-sheet"
|
|
753
|
+
defaultWidth={640}
|
|
754
|
+
minWidth={500}
|
|
755
|
+
className="gap-0 overflow-hidden bg-[radial-gradient(circle_at_top,_hsl(var(--primary)/0.09),_transparent_38%),linear-gradient(180deg,_hsl(var(--background)),_hsl(var(--muted)/0.18))]"
|
|
756
|
+
>
|
|
757
|
+
<SheetHeader className="border-b border-border/60 bg-background/85 px-6 pb-5 pt-6 backdrop-blur">
|
|
758
|
+
<div className="flex items-start justify-between gap-4">
|
|
759
|
+
<div className="space-y-2">
|
|
760
|
+
<div className="inline-flex items-center gap-2 rounded-full border border-primary/15 bg-primary/5 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-primary/80">
|
|
761
|
+
<Cog className="h-3.5 w-3.5" />
|
|
762
|
+
Bulk Upload Control
|
|
763
|
+
</div>
|
|
764
|
+
<SheetTitle className="text-lg font-semibold tracking-tight">
|
|
765
|
+
Configurações do upload em massa
|
|
766
|
+
</SheetTitle>
|
|
767
|
+
<SheetDescription className="max-w-xl text-sm text-muted-foreground">
|
|
768
|
+
Configure bucket, credenciais temporárias e webhook para o
|
|
769
|
+
fluxo de upload em massa com provisionamento automático da
|
|
770
|
+
integração.
|
|
771
|
+
</SheetDescription>
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
<div className="hidden rounded-2xl border border-border/60 bg-background/80 px-4 py-3 shadow-sm md:block">
|
|
775
|
+
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
776
|
+
Estado atual
|
|
777
|
+
</p>
|
|
778
|
+
<p className="mt-1 text-sm font-semibold text-foreground">
|
|
779
|
+
{currentStatusLabel}
|
|
780
|
+
</p>
|
|
781
|
+
<p className="mt-1 max-w-[15rem] text-xs text-muted-foreground">
|
|
782
|
+
{currentStatusDescription}
|
|
783
|
+
</p>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
</SheetHeader>
|
|
787
|
+
|
|
788
|
+
<ScrollArea className="flex-1 overflow-hidden px-6">
|
|
789
|
+
<div className="space-y-6 py-6">
|
|
790
|
+
<section className="overflow-hidden rounded-2xl border border-border/60 bg-background/90 shadow-sm">
|
|
791
|
+
<div className="border-b border-border/50 bg-gradient-to-r from-primary/8 via-primary/4 to-transparent px-5 py-4">
|
|
792
|
+
<h3 className="text-sm font-semibold tracking-tight">
|
|
793
|
+
Instruções rápidas
|
|
794
|
+
</h3>
|
|
795
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
796
|
+
A configuração abaixo valida o acesso ao bucket e prepara a
|
|
797
|
+
integração do desktop automaticamente.
|
|
798
|
+
</p>
|
|
799
|
+
</div>
|
|
800
|
+
<ul className="space-y-2 px-5 py-4 text-xs text-muted-foreground">
|
|
801
|
+
<li>
|
|
802
|
+
1. Informe abaixo a role de execução da Lambda em{' '}
|
|
803
|
+
<span className="font-mono">
|
|
804
|
+
{BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
|
|
805
|
+
</span>{' '}
|
|
806
|
+
no grupo LMS.
|
|
807
|
+
</li>
|
|
808
|
+
<li>
|
|
809
|
+
2. Selecione um perfil de integração do tipo storage (AWS)
|
|
810
|
+
com bucket em{' '}
|
|
811
|
+
<span className="font-mono">config.bucket</span>.
|
|
812
|
+
</li>
|
|
813
|
+
<li>
|
|
814
|
+
3. Salve a configuração para criar/atualizar Lambda,
|
|
815
|
+
permissões do S3 e webhook automaticamente.
|
|
816
|
+
</li>
|
|
817
|
+
<li>
|
|
818
|
+
4. Copie URL + token do webhook para o sistema externo.
|
|
819
|
+
</li>
|
|
820
|
+
</ul>
|
|
821
|
+
</section>
|
|
822
|
+
|
|
823
|
+
<section className="space-y-4 rounded-2xl border border-border/60 bg-background/90 p-5 shadow-sm">
|
|
824
|
+
<div className="space-y-1">
|
|
825
|
+
<h3 className="text-sm font-semibold tracking-tight">
|
|
826
|
+
Fonte dos arquivos
|
|
827
|
+
</h3>
|
|
828
|
+
<p className="text-xs text-muted-foreground">
|
|
829
|
+
Escolha o bucket e o perfil S3 que serão usados para
|
|
830
|
+
autenticar uploads e sincronizar a Lambda externa.
|
|
831
|
+
</p>
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
<div className="space-y-2">
|
|
835
|
+
<div className="rounded-xl border border-amber-300/40 bg-amber-500/10 p-3">
|
|
836
|
+
<p className="text-xs font-medium text-amber-800 dark:text-amber-300">
|
|
837
|
+
Role de execução da Lambda
|
|
838
|
+
</p>
|
|
839
|
+
<p className="mt-1 text-xs text-amber-800/90 dark:text-amber-300/90">
|
|
840
|
+
Essa role será usada para criar a Lambda automaticamente
|
|
841
|
+
quando necessário. Informe o ARN completo da role em{' '}
|
|
842
|
+
<span className="font-mono">
|
|
843
|
+
{BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
|
|
844
|
+
</span>{' '}
|
|
845
|
+
antes de salvar a configuração.
|
|
846
|
+
</p>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<div className="space-y-2">
|
|
850
|
+
<Label htmlFor="bulk-upload-lambda-role-arn">
|
|
851
|
+
ARN da role da Lambda
|
|
852
|
+
</Label>
|
|
853
|
+
<Input
|
|
854
|
+
id="bulk-upload-lambda-role-arn"
|
|
855
|
+
placeholder="arn:aws:iam::123456789012:role/lms-bulk-upload-lambda-role"
|
|
856
|
+
value={lambdaRoleArn}
|
|
857
|
+
onChange={(event) => setLambdaRoleArn(event.target.value)}
|
|
858
|
+
disabled={settingsLoading || isSavingSettings}
|
|
859
|
+
/>
|
|
860
|
+
<p className="text-xs text-muted-foreground">
|
|
861
|
+
Setting: {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
|
|
862
|
+
</p>
|
|
863
|
+
</div>
|
|
864
|
+
|
|
865
|
+
<Label>Perfil de integração de storage</Label>
|
|
866
|
+
<div className="rounded-2xl border border-border/60 bg-muted/20 p-3">
|
|
867
|
+
<div className="flex items-center gap-2">
|
|
868
|
+
<EntityPicker<IntegrationProfileOption>
|
|
869
|
+
className="flex-1"
|
|
870
|
+
buttonClassName="border-border/60 bg-background shadow-sm"
|
|
871
|
+
placeholder="Selecione um perfil de integração"
|
|
872
|
+
options={storageProfileOptions}
|
|
873
|
+
value={storageProfileId}
|
|
874
|
+
valueType="number"
|
|
875
|
+
getOptionValue={(option) => option.id}
|
|
876
|
+
getOptionLabel={(option) => option.name}
|
|
877
|
+
getOptionDescription={(option) => option.slug}
|
|
878
|
+
onChange={(value) =>
|
|
879
|
+
setStorageProfileId(
|
|
880
|
+
value === null ? null : Number(value)
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
clearable
|
|
884
|
+
searchable
|
|
885
|
+
showCreateButton={false}
|
|
886
|
+
loadingLabel={
|
|
887
|
+
profilesLoading ? 'Carregando...' : undefined
|
|
888
|
+
}
|
|
889
|
+
/>
|
|
890
|
+
|
|
891
|
+
<TooltipProvider>
|
|
892
|
+
<Tooltip>
|
|
893
|
+
<TooltipTrigger asChild>
|
|
894
|
+
<Button
|
|
895
|
+
type="button"
|
|
896
|
+
variant="outline"
|
|
897
|
+
size="icon"
|
|
898
|
+
className="shrink-0 border-border/60 bg-background shadow-sm"
|
|
899
|
+
onClick={openCreateProfileSheet}
|
|
900
|
+
aria-label="Criar perfil"
|
|
901
|
+
>
|
|
902
|
+
<Plus className="h-4 w-4" />
|
|
903
|
+
</Button>
|
|
904
|
+
</TooltipTrigger>
|
|
905
|
+
<TooltipContent>Criar perfil</TooltipContent>
|
|
906
|
+
</Tooltip>
|
|
907
|
+
|
|
908
|
+
{selectedStorageProfile ? (
|
|
909
|
+
<Tooltip>
|
|
910
|
+
<TooltipTrigger asChild>
|
|
911
|
+
<Button
|
|
912
|
+
type="button"
|
|
913
|
+
variant="outline"
|
|
914
|
+
size="icon"
|
|
915
|
+
className="shrink-0 border-border/60 bg-background shadow-sm"
|
|
916
|
+
onClick={openEditProfileSheet}
|
|
917
|
+
aria-label="Editar perfil"
|
|
918
|
+
>
|
|
919
|
+
<Pencil className="h-4 w-4" />
|
|
920
|
+
</Button>
|
|
921
|
+
</TooltipTrigger>
|
|
922
|
+
<TooltipContent>Editar perfil</TooltipContent>
|
|
923
|
+
</Tooltip>
|
|
924
|
+
) : null}
|
|
925
|
+
</TooltipProvider>
|
|
926
|
+
</div>
|
|
927
|
+
|
|
928
|
+
<p className="mt-3 text-xs text-muted-foreground">
|
|
929
|
+
O seletor já permite limpar a seleção atual. Use os
|
|
930
|
+
atalhos laterais apenas para criar ou editar o perfil
|
|
931
|
+
ativo.
|
|
932
|
+
</p>
|
|
933
|
+
</div>
|
|
934
|
+
|
|
935
|
+
<p className="text-xs text-muted-foreground">
|
|
936
|
+
Setting: {BULK_UPLOAD_STORAGE_PROFILE_SLUG}
|
|
937
|
+
</p>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<div className="space-y-2">
|
|
941
|
+
<Label>Bucket S3 (do perfil selecionado)</Label>
|
|
942
|
+
<Input
|
|
943
|
+
readOnly
|
|
944
|
+
value={
|
|
945
|
+
bucketName ||
|
|
946
|
+
'Selecione e salve um perfil para carregar o bucket'
|
|
947
|
+
}
|
|
948
|
+
disabled={settingsLoading}
|
|
949
|
+
/>
|
|
950
|
+
<p className="text-xs text-muted-foreground">
|
|
951
|
+
O bucket vem do campo{' '}
|
|
952
|
+
<span className="font-mono">config.bucket</span> do perfil
|
|
953
|
+
de integração.
|
|
954
|
+
</p>
|
|
955
|
+
</div>
|
|
956
|
+
|
|
957
|
+
<div className="rounded-2xl border border-dashed border-border/70 bg-muted/15 p-4">
|
|
958
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
959
|
+
<div className="space-y-1">
|
|
960
|
+
<p className="text-sm font-medium">Sincronização final</p>
|
|
961
|
+
<p className="text-xs text-muted-foreground">
|
|
962
|
+
O salvamento valida o bucket, testa as credenciais
|
|
963
|
+
temporárias e atualiza o webhook usado pela Lambda.
|
|
964
|
+
</p>
|
|
965
|
+
</div>
|
|
966
|
+
|
|
967
|
+
{storageProfileId && lambdaRoleArn.trim() ? (
|
|
968
|
+
<Button
|
|
969
|
+
type="button"
|
|
970
|
+
onClick={saveBulkUploadSettings}
|
|
971
|
+
disabled={isSavingSettings}
|
|
972
|
+
className="gap-2 shadow-sm"
|
|
973
|
+
>
|
|
974
|
+
{isSavingSettings ? (
|
|
975
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
976
|
+
) : (
|
|
977
|
+
<Save className="h-4 w-4" />
|
|
978
|
+
)}
|
|
979
|
+
Salvar configuração
|
|
980
|
+
</Button>
|
|
981
|
+
) : (
|
|
982
|
+
<p className="text-xs text-muted-foreground">
|
|
983
|
+
Informe a role ARN e selecione um perfil para habilitar
|
|
984
|
+
o salvamento.
|
|
985
|
+
</p>
|
|
986
|
+
)}
|
|
987
|
+
</div>
|
|
988
|
+
|
|
989
|
+
{lastSetupFeedback ? (
|
|
990
|
+
<div className="mt-3 rounded-xl border border-emerald-400/30 bg-emerald-500/10 p-3">
|
|
991
|
+
<p className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
|
|
992
|
+
Tudo pronto
|
|
993
|
+
</p>
|
|
994
|
+
<p className="mt-1 text-xs text-emerald-700/90 dark:text-emerald-300/90">
|
|
995
|
+
Função Lambda: {lastSetupFeedback.lambdaName}
|
|
996
|
+
</p>
|
|
997
|
+
<p className="text-xs text-emerald-700/90 dark:text-emerald-300/90">
|
|
998
|
+
Configurado em: {lastSetupFeedback.configuredAt}
|
|
999
|
+
</p>
|
|
1000
|
+
</div>
|
|
1001
|
+
) : null}
|
|
1002
|
+
</div>
|
|
1003
|
+
</section>
|
|
1004
|
+
|
|
1005
|
+
<section className="space-y-4">
|
|
1006
|
+
<div className="flex items-center justify-between gap-2">
|
|
1007
|
+
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
|
1008
|
+
<Webhook className="h-4 w-4" />
|
|
1009
|
+
Webhook de integração
|
|
1010
|
+
</h3>
|
|
1011
|
+
|
|
1012
|
+
{webhookIntegration ? (
|
|
1013
|
+
<Badge
|
|
1014
|
+
variant={
|
|
1015
|
+
webhookIntegration.status === 'active'
|
|
1016
|
+
? 'default'
|
|
1017
|
+
: 'outline'
|
|
1018
|
+
}
|
|
1019
|
+
>
|
|
1020
|
+
{webhookIntegration.status === 'active'
|
|
1021
|
+
? 'Ativo'
|
|
1022
|
+
: 'Inativo'}
|
|
1023
|
+
</Badge>
|
|
1024
|
+
) : null}
|
|
1025
|
+
</div>
|
|
1026
|
+
|
|
1027
|
+
{!webhookIntegration ? (
|
|
1028
|
+
<div className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
|
1029
|
+
<p className="text-sm text-muted-foreground">
|
|
1030
|
+
O webhook será criado automaticamente após salvar uma
|
|
1031
|
+
configuração válida (bucket + perfil), com URL e segredo
|
|
1032
|
+
exibidos abaixo.
|
|
1033
|
+
</p>
|
|
1034
|
+
</div>
|
|
1035
|
+
) : (
|
|
1036
|
+
<div className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
|
1037
|
+
<div className="space-y-1">
|
|
1038
|
+
<Label>URL do webhook</Label>
|
|
1039
|
+
<div className="flex items-center gap-2">
|
|
1040
|
+
<Input
|
|
1041
|
+
readOnly
|
|
1042
|
+
value={webhookIntegration.public_url ?? ''}
|
|
1043
|
+
/>
|
|
1044
|
+
<CopyButton
|
|
1045
|
+
value={webhookIntegration.public_url ?? ''}
|
|
1046
|
+
/>
|
|
1047
|
+
<TooltipProvider>
|
|
1048
|
+
<Tooltip>
|
|
1049
|
+
<TooltipTrigger asChild>
|
|
1050
|
+
<Button
|
|
1051
|
+
type="button"
|
|
1052
|
+
variant="outline"
|
|
1053
|
+
size="icon"
|
|
1054
|
+
onClick={() =>
|
|
1055
|
+
window.open(
|
|
1056
|
+
'/core/integrations-webhooks',
|
|
1057
|
+
'_blank'
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
aria-label="Abrir webhooks"
|
|
1061
|
+
>
|
|
1062
|
+
<ExternalLink className="h-4 w-4" />
|
|
1063
|
+
</Button>
|
|
1064
|
+
</TooltipTrigger>
|
|
1065
|
+
<TooltipContent>
|
|
1066
|
+
Abrir tela de webhooks
|
|
1067
|
+
</TooltipContent>
|
|
1068
|
+
</Tooltip>
|
|
1069
|
+
</TooltipProvider>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
|
|
1073
|
+
<div className="space-y-1">
|
|
1074
|
+
<Label>Token do webhook</Label>
|
|
1075
|
+
<div className="flex items-center gap-2">
|
|
1076
|
+
<Input
|
|
1077
|
+
readOnly
|
|
1078
|
+
value={
|
|
1079
|
+
webhookPlainToken ??
|
|
1080
|
+
'Token oculto. Regenerar para visualizar novo token.'
|
|
1081
|
+
}
|
|
1082
|
+
/>
|
|
1083
|
+
{webhookPlainToken ? (
|
|
1084
|
+
<CopyButton value={webhookPlainToken} />
|
|
1085
|
+
) : null}
|
|
1086
|
+
<Button
|
|
1087
|
+
type="button"
|
|
1088
|
+
variant="outline"
|
|
1089
|
+
className="gap-2"
|
|
1090
|
+
onClick={regenerateWebhookToken}
|
|
1091
|
+
disabled={isRegeneratingToken}
|
|
1092
|
+
>
|
|
1093
|
+
{isRegeneratingToken ? (
|
|
1094
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1095
|
+
) : (
|
|
1096
|
+
<RefreshCw className="h-4 w-4" />
|
|
1097
|
+
)}
|
|
1098
|
+
Regenerar token
|
|
1099
|
+
</Button>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
)}
|
|
1104
|
+
</section>
|
|
1105
|
+
</div>
|
|
1106
|
+
</ScrollArea>
|
|
1107
|
+
</ResizableSheetContent>
|
|
1108
|
+
</Sheet>
|
|
1109
|
+
|
|
1110
|
+
<AlertDialog
|
|
1111
|
+
open={isCleanupDialogOpen}
|
|
1112
|
+
onOpenChange={(open) => {
|
|
1113
|
+
if (!isCleaningHistory) {
|
|
1114
|
+
setIsCleanupDialogOpen(open);
|
|
1115
|
+
}
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
<AlertDialogContent>
|
|
1119
|
+
<AlertDialogHeader>
|
|
1120
|
+
<AlertDialogTitle>Limpar historico de uploads</AlertDialogTitle>
|
|
1121
|
+
<AlertDialogDescription>
|
|
1122
|
+
Selecione quais status e intervalo de tempo devem ser removidos.
|
|
1123
|
+
Esta acao remove apenas registros do banco e nao afeta arquivos no
|
|
1124
|
+
S3.
|
|
1125
|
+
</AlertDialogDescription>
|
|
1126
|
+
</AlertDialogHeader>
|
|
1127
|
+
|
|
1128
|
+
<div className="space-y-4 py-2">
|
|
1129
|
+
<div className="space-y-2">
|
|
1130
|
+
<Label>Status para limpar</Label>
|
|
1131
|
+
<div className="space-y-2 rounded-lg border border-border/70 bg-muted/20 p-3">
|
|
1132
|
+
{CLEANUP_STATUS_OPTIONS.map((option) => (
|
|
1133
|
+
<div key={option.value} className="flex items-center gap-2">
|
|
1134
|
+
<Checkbox
|
|
1135
|
+
id={`cleanup-status-${option.value}`}
|
|
1136
|
+
checked={cleanupStatuses.has(option.value)}
|
|
1137
|
+
onCheckedChange={() => toggleCleanupStatus(option.value)}
|
|
1138
|
+
disabled={isCleaningHistory}
|
|
1139
|
+
/>
|
|
1140
|
+
<Label
|
|
1141
|
+
htmlFor={`cleanup-status-${option.value}`}
|
|
1142
|
+
className="cursor-pointer text-sm font-normal"
|
|
1143
|
+
>
|
|
1144
|
+
{option.label}
|
|
1145
|
+
</Label>
|
|
1146
|
+
</div>
|
|
1147
|
+
))}
|
|
1148
|
+
</div>
|
|
1149
|
+
</div>
|
|
1150
|
+
|
|
1151
|
+
<div className="space-y-2">
|
|
1152
|
+
<Label>Periodo da limpeza</Label>
|
|
1153
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
1154
|
+
{CLEANUP_TIME_WINDOW_OPTIONS.map((option) => (
|
|
1155
|
+
<button
|
|
1156
|
+
key={option.value}
|
|
1157
|
+
type="button"
|
|
1158
|
+
onClick={() => setCleanupTimeWindow(option.value)}
|
|
1159
|
+
className={cn(
|
|
1160
|
+
'rounded-lg border p-3 text-left transition-colors',
|
|
1161
|
+
cleanupTimeWindow === option.value
|
|
1162
|
+
? 'border-primary bg-primary/10'
|
|
1163
|
+
: 'border-border/70 bg-background hover:border-primary/40'
|
|
1164
|
+
)}
|
|
1165
|
+
disabled={isCleaningHistory}
|
|
1166
|
+
>
|
|
1167
|
+
<p className="text-sm font-medium">{option.label}</p>
|
|
1168
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
1169
|
+
{option.description}
|
|
1170
|
+
</p>
|
|
1171
|
+
</button>
|
|
1172
|
+
))}
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
|
|
1176
|
+
<div className="rounded-lg border border-amber-300/50 bg-amber-500/10 p-3 text-xs text-amber-900 dark:text-amber-200">
|
|
1177
|
+
Esta operacao e irreversivel e pode remover muitos registros de
|
|
1178
|
+
uma vez.
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
|
|
1182
|
+
<div className="flex justify-end gap-2">
|
|
1183
|
+
<AlertDialogCancel disabled={isCleaningHistory}>
|
|
1184
|
+
Cancelar
|
|
1185
|
+
</AlertDialogCancel>
|
|
1186
|
+
<AlertDialogAction
|
|
1187
|
+
className="bg-red-600 text-white hover:bg-red-700"
|
|
1188
|
+
disabled={
|
|
1189
|
+
isCleaningHistory || cleanupStatusSelection.length === 0
|
|
1190
|
+
}
|
|
1191
|
+
onClick={(event) => {
|
|
1192
|
+
event.preventDefault();
|
|
1193
|
+
runCleanupHistory();
|
|
1194
|
+
}}
|
|
1195
|
+
>
|
|
1196
|
+
{isCleaningHistory ? (
|
|
1197
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1198
|
+
) : (
|
|
1199
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
1200
|
+
)}
|
|
1201
|
+
Limpar historico
|
|
1202
|
+
</AlertDialogAction>
|
|
1203
|
+
</div>
|
|
1204
|
+
</AlertDialogContent>
|
|
1205
|
+
</AlertDialog>
|
|
1206
|
+
|
|
1207
|
+
<IntegrationProfileSheet
|
|
1208
|
+
open={isProfileSheetOpen}
|
|
1209
|
+
onOpenChange={setIsProfileSheetOpen}
|
|
1210
|
+
profileId={editingProfileId}
|
|
1211
|
+
lockedTypeSlug="storage"
|
|
1212
|
+
lockedProviderSlug="s3"
|
|
1213
|
+
sheetId="lms-bulk-upload-storage-profile-editor"
|
|
1214
|
+
defaultWidth={760}
|
|
1215
|
+
onSaved={handleProfileSaved}
|
|
1216
|
+
/>
|
|
1217
|
+
|
|
1218
|
+
<KpiCardsGrid items={kpiItems} columns={3} />
|
|
1219
|
+
|
|
1220
|
+
<div className="flex items-center justify-between gap-3">
|
|
1221
|
+
<SearchBar
|
|
1222
|
+
searchQuery={searchInput}
|
|
1223
|
+
onSearchChange={setSearchInput}
|
|
1224
|
+
onSearch={() => {
|
|
1225
|
+
setSearch(searchInput);
|
|
1226
|
+
setPage(1);
|
|
1227
|
+
}}
|
|
1228
|
+
placeholder="Buscar por arquivo, app ou usuario"
|
|
1229
|
+
controls={[
|
|
1230
|
+
{
|
|
1231
|
+
id: 'status-filter',
|
|
1232
|
+
type: 'select',
|
|
1233
|
+
value: statusFilter,
|
|
1234
|
+
onChange: (value) => {
|
|
1235
|
+
setStatusFilter(value);
|
|
1236
|
+
setPage(1);
|
|
1237
|
+
},
|
|
1238
|
+
options: [
|
|
1239
|
+
{ value: 'all', label: 'Todos os status' },
|
|
1240
|
+
{ value: 'queued', label: 'Na fila' },
|
|
1241
|
+
{ value: 'uploading', label: 'Enviando' },
|
|
1242
|
+
{ value: 'cancelling', label: 'Cancelando' },
|
|
1243
|
+
{ value: 'received', label: 'Arquivo recebido' },
|
|
1244
|
+
{ value: 'done', label: 'Concluido' },
|
|
1245
|
+
{ value: 'cancelled', label: 'Cancelado' },
|
|
1246
|
+
{ value: 'error', label: 'Erro' },
|
|
1247
|
+
],
|
|
1248
|
+
},
|
|
1249
|
+
]}
|
|
1250
|
+
/>
|
|
1251
|
+
</div>
|
|
1252
|
+
|
|
1253
|
+
<div className="overflow-hidden rounded-xl border border-border bg-card">
|
|
1254
|
+
<Table>
|
|
1255
|
+
<TableHeader>
|
|
1256
|
+
<TableRow>
|
|
1257
|
+
<TableHead>Arquivo</TableHead>
|
|
1258
|
+
<TableHead>Tamanho</TableHead>
|
|
1259
|
+
<TableHead>Usuario</TableHead>
|
|
1260
|
+
<TableHead>Aula vinculada</TableHead>
|
|
1261
|
+
<TableHead>Status</TableHead>
|
|
1262
|
+
<TableHead className="w-40">Progresso</TableHead>
|
|
1263
|
+
<TableHead>Atualizado em</TableHead>
|
|
1264
|
+
</TableRow>
|
|
1265
|
+
</TableHeader>
|
|
1266
|
+
<TableBody>
|
|
1267
|
+
{isLoading ? (
|
|
1268
|
+
Array.from({ length: 8 }).map((_, index) => (
|
|
1269
|
+
<TableRow key={`skeleton-${index}`}>
|
|
1270
|
+
<TableCell colSpan={7}>
|
|
1271
|
+
<Skeleton className="h-6 w-full" />
|
|
1272
|
+
</TableCell>
|
|
1273
|
+
</TableRow>
|
|
1274
|
+
))
|
|
1275
|
+
) : rows.length === 0 ? (
|
|
1276
|
+
<TableRow>
|
|
1277
|
+
<TableCell colSpan={7} className="py-8">
|
|
1278
|
+
<EmptyState
|
|
1279
|
+
icon={<HardDriveUpload className="h-5 w-5" />}
|
|
1280
|
+
title="Nenhum upload encontrado"
|
|
1281
|
+
description="Inicie uploads no Hedhog Desktop para visualizar o historico e andamento aqui."
|
|
1282
|
+
/>
|
|
1283
|
+
</TableCell>
|
|
1284
|
+
</TableRow>
|
|
1285
|
+
) : (
|
|
1286
|
+
rows.map((row) => {
|
|
1287
|
+
const meta = getStatusMeta(row.status);
|
|
1288
|
+
const StatusIcon = meta.icon;
|
|
1289
|
+
const pct = Math.max(0, Math.min(100, row.progressPercent));
|
|
1290
|
+
|
|
1291
|
+
return (
|
|
1292
|
+
<TableRow key={row.id} className="cursor-pointer">
|
|
1293
|
+
<TableCell className="max-w-80">
|
|
1294
|
+
<div className="flex items-start gap-2.5">
|
|
1295
|
+
<span className="mt-0.5 shrink-0">
|
|
1296
|
+
<FileTypeIcon filename={row.fileName} size={20} />
|
|
1297
|
+
</span>
|
|
1298
|
+
<div className="min-w-0 space-y-0.5">
|
|
1299
|
+
<p className="truncate text-sm font-medium">
|
|
1300
|
+
{row.fileName}
|
|
1301
|
+
</p>
|
|
1302
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1303
|
+
Sessao #{row.sessionId} · {row.appName}
|
|
1304
|
+
</p>
|
|
1305
|
+
{row.errorMessage ? (
|
|
1306
|
+
<p className="truncate text-xs text-destructive">
|
|
1307
|
+
{row.errorMessage}
|
|
1308
|
+
</p>
|
|
1309
|
+
) : null}
|
|
1310
|
+
</div>
|
|
1311
|
+
</div>
|
|
1312
|
+
</TableCell>
|
|
1313
|
+
|
|
1314
|
+
<TableCell className="tabular-nums text-sm">
|
|
1315
|
+
{formatBytes(row.sizeBytes)}
|
|
1316
|
+
</TableCell>
|
|
1317
|
+
|
|
1318
|
+
<TableCell>
|
|
1319
|
+
<div className="flex items-center gap-2">
|
|
1320
|
+
<Avatar className="h-7 w-7 shrink-0">
|
|
1321
|
+
<AvatarImage
|
|
1322
|
+
src={getPhotoUrl(row.userPhotoId)}
|
|
1323
|
+
alt={row.userName ?? ''}
|
|
1324
|
+
/>
|
|
1325
|
+
<AvatarFallback className="bg-muted text-[10px] font-medium">
|
|
1326
|
+
{row.userName
|
|
1327
|
+
? row.userName
|
|
1328
|
+
.split(' ')
|
|
1329
|
+
.slice(0, 2)
|
|
1330
|
+
.map((part) => part[0])
|
|
1331
|
+
.join('')
|
|
1332
|
+
.toUpperCase()
|
|
1333
|
+
: 'U'}
|
|
1334
|
+
</AvatarFallback>
|
|
1335
|
+
</Avatar>
|
|
1336
|
+
<div className="min-w-0">
|
|
1337
|
+
<p className="truncate text-sm">
|
|
1338
|
+
{row.userName || 'Usuario nao identificado'}
|
|
1339
|
+
</p>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
</TableCell>
|
|
1343
|
+
|
|
1344
|
+
<TableCell className="max-w-[22rem]">
|
|
1345
|
+
{row.matchedCourseId && row.matchedLessonId ? (
|
|
1346
|
+
<button
|
|
1347
|
+
type="button"
|
|
1348
|
+
className="flex w-full items-center gap-2 text-left"
|
|
1349
|
+
onClick={() =>
|
|
1350
|
+
window.open(
|
|
1351
|
+
`/lms/courses/${row.matchedCourseId}`,
|
|
1352
|
+
'_blank',
|
|
1353
|
+
'noopener,noreferrer'
|
|
1354
|
+
)
|
|
1355
|
+
}
|
|
1356
|
+
>
|
|
1357
|
+
<Avatar className="h-7 w-7 shrink-0 rounded-md">
|
|
1358
|
+
<AvatarImage
|
|
1359
|
+
src={getPhotoUrl(row.matchedCourseLogoFileId)}
|
|
1360
|
+
alt={row.matchedCourseTitle ?? ''}
|
|
1361
|
+
/>
|
|
1362
|
+
<AvatarFallback className="rounded-md bg-muted text-[10px] font-medium">
|
|
1363
|
+
{(row.matchedCourseTitle ?? 'Curso')
|
|
1364
|
+
.split(' ')
|
|
1365
|
+
.slice(0, 2)
|
|
1366
|
+
.map((part) => part[0])
|
|
1367
|
+
.join('')
|
|
1368
|
+
.toUpperCase()}
|
|
1369
|
+
</AvatarFallback>
|
|
1370
|
+
</Avatar>
|
|
1371
|
+
<div className="min-w-0 space-y-0.5">
|
|
1372
|
+
<p className="truncate text-sm font-medium text-primary underline-offset-4 hover:underline">
|
|
1373
|
+
{row.matchedLessonTitle ??
|
|
1374
|
+
`Aula #${row.matchedLessonId}`}
|
|
1375
|
+
</p>
|
|
1376
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1377
|
+
{row.matchedSessionTitle
|
|
1378
|
+
? `Sessão ${row.matchedSessionTitle}`
|
|
1379
|
+
: `Sessão #${row.matchedSessionId ?? '-'}`}{' '}
|
|
1380
|
+
·{' '}
|
|
1381
|
+
{row.matchedCourseTitle ??
|
|
1382
|
+
row.matchedCourseSlug ??
|
|
1383
|
+
'Curso não identificado'}
|
|
1384
|
+
</p>
|
|
1385
|
+
</div>
|
|
1386
|
+
</button>
|
|
1387
|
+
) : (
|
|
1388
|
+
<span className="text-sm text-muted-foreground">-</span>
|
|
1389
|
+
)}
|
|
1390
|
+
</TableCell>
|
|
1391
|
+
|
|
1392
|
+
<TableCell>
|
|
1393
|
+
<div className="flex items-center gap-1.5">
|
|
1394
|
+
<StatusIcon
|
|
1395
|
+
className={cn('h-3.5 w-3.5 shrink-0', meta.iconClass)}
|
|
1396
|
+
/>
|
|
1397
|
+
<Badge variant={meta.variant}>{meta.label}</Badge>
|
|
1398
|
+
</div>
|
|
1399
|
+
</TableCell>
|
|
1400
|
+
|
|
1401
|
+
<TableCell>
|
|
1402
|
+
<div className="space-y-1">
|
|
1403
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
1404
|
+
<span
|
|
1405
|
+
className={cn(
|
|
1406
|
+
'tabular-nums font-medium',
|
|
1407
|
+
meta.animated && 'text-foreground'
|
|
1408
|
+
)}
|
|
1409
|
+
>
|
|
1410
|
+
{pct}%
|
|
1411
|
+
</span>
|
|
1412
|
+
</div>
|
|
1413
|
+
<Progress
|
|
1414
|
+
value={pct}
|
|
1415
|
+
className={cn(
|
|
1416
|
+
'h-1.5',
|
|
1417
|
+
meta.animated &&
|
|
1418
|
+
'[&>[data-slot=progress-indicator]]:animate-pulse'
|
|
1419
|
+
)}
|
|
1420
|
+
/>
|
|
1421
|
+
</div>
|
|
1422
|
+
</TableCell>
|
|
1423
|
+
|
|
1424
|
+
<TableCell>
|
|
1425
|
+
<div className="space-y-0.5">
|
|
1426
|
+
<p className="text-sm">{formatDate(row.updatedAt)}</p>
|
|
1427
|
+
<p className="text-xs text-muted-foreground">
|
|
1428
|
+
Finalizado: {formatDate(row.completedAt)}
|
|
1429
|
+
</p>
|
|
1430
|
+
</div>
|
|
1431
|
+
</TableCell>
|
|
1432
|
+
</TableRow>
|
|
1433
|
+
);
|
|
1434
|
+
})
|
|
1435
|
+
)}
|
|
1436
|
+
</TableBody>
|
|
1437
|
+
</Table>
|
|
1438
|
+
</div>
|
|
1439
|
+
|
|
1440
|
+
<PaginationFooter
|
|
1441
|
+
currentPage={page}
|
|
1442
|
+
pageSize={pageSize}
|
|
1443
|
+
totalItems={total}
|
|
1444
|
+
onPageChange={setPage}
|
|
1445
|
+
onPageSizeChange={(nextPageSize) => {
|
|
1446
|
+
setPageSize(nextPageSize);
|
|
1447
|
+
setPage(1);
|
|
1448
|
+
}}
|
|
1449
|
+
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
1450
|
+
/>
|
|
1451
|
+
</Page>
|
|
1452
|
+
);
|
|
1453
|
+
}
|