@hed-hog/lms 0.0.304 → 0.0.306
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/README.md +413 -401
- package/dist/certificate/certificate.controller.d.ts +90 -0
- package/dist/certificate/certificate.controller.d.ts.map +1 -0
- package/dist/certificate/certificate.controller.js +121 -0
- package/dist/certificate/certificate.controller.js.map +1 -0
- package/dist/certificate/certificate.module.d.ts +3 -0
- package/dist/certificate/certificate.module.d.ts.map +1 -0
- package/dist/certificate/certificate.module.js +26 -0
- package/dist/certificate/certificate.module.js.map +1 -0
- package/dist/certificate/certificate.service.d.ts +115 -0
- package/dist/certificate/certificate.service.d.ts.map +1 -0
- package/dist/certificate/certificate.service.js +343 -0
- package/dist/certificate/certificate.service.js.map +1 -0
- package/dist/certificate/dto/create-certificate-template.dto.d.ts +8 -0
- package/dist/certificate/dto/create-certificate-template.dto.d.ts.map +1 -0
- package/dist/certificate/dto/create-certificate-template.dto.js +44 -0
- package/dist/certificate/dto/create-certificate-template.dto.js.map +1 -0
- package/dist/certificate/dto/update-certificate-template.dto.d.ts +6 -0
- package/dist/certificate/dto/update-certificate-template.dto.d.ts.map +1 -0
- package/dist/certificate/dto/update-certificate-template.dto.js +9 -0
- package/dist/certificate/dto/update-certificate-template.dto.js.map +1 -0
- package/dist/class-group/class-group.controller.d.ts +305 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -0
- package/dist/class-group/class-group.controller.js +257 -0
- package/dist/class-group/class-group.controller.js.map +1 -0
- package/dist/class-group/class-group.module.d.ts +3 -0
- package/dist/class-group/class-group.module.d.ts.map +1 -0
- package/dist/class-group/class-group.module.js +25 -0
- package/dist/class-group/class-group.module.js.map +1 -0
- package/dist/class-group/class-group.service.d.ts +354 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -0
- package/dist/class-group/class-group.service.js +1356 -0
- package/dist/class-group/class-group.service.js.map +1 -0
- package/dist/class-group/dto/create-class-group.dto.d.ts +33 -0
- package/dist/class-group/dto/create-class-group.dto.d.ts.map +1 -0
- package/dist/class-group/dto/create-class-group.dto.js +165 -0
- package/dist/class-group/dto/create-class-group.dto.js.map +1 -0
- package/dist/class-group/dto/create-session.dto.d.ts +22 -0
- package/dist/class-group/dto/create-session.dto.d.ts.map +1 -0
- package/dist/class-group/dto/create-session.dto.js +117 -0
- package/dist/class-group/dto/create-session.dto.js.map +1 -0
- package/dist/class-group/dto/enrollment.dto.d.ts +22 -0
- package/dist/class-group/dto/enrollment.dto.d.ts.map +1 -0
- package/dist/class-group/dto/enrollment.dto.js +89 -0
- package/dist/class-group/dto/enrollment.dto.js.map +1 -0
- package/dist/class-group/dto/update-class-group.dto.d.ts +6 -0
- package/dist/class-group/dto/update-class-group.dto.d.ts.map +1 -0
- package/dist/class-group/dto/update-class-group.dto.js +9 -0
- package/dist/class-group/dto/update-class-group.dto.js.map +1 -0
- package/dist/class-group/dto/update-session.dto.d.ts +7 -0
- package/dist/class-group/dto/update-session.dto.d.ts.map +1 -0
- package/dist/class-group/dto/update-session.dto.js +24 -0
- package/dist/class-group/dto/update-session.dto.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +127 -0
- package/dist/course/course-structure.controller.d.ts.map +1 -0
- package/dist/course/course-structure.controller.js +115 -0
- package/dist/course/course-structure.controller.js.map +1 -0
- package/dist/course/course-structure.service.d.ts +142 -0
- package/dist/course/course-structure.service.d.ts.map +1 -0
- package/dist/course/course-structure.service.js +445 -0
- package/dist/course/course-structure.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +195 -0
- package/dist/course/course.controller.d.ts.map +1 -0
- package/dist/course/course.controller.js +104 -0
- package/dist/course/course.controller.js.map +1 -0
- package/dist/course/course.module.d.ts +3 -0
- package/dist/course/course.module.d.ts.map +1 -0
- package/dist/course/course.module.js +28 -0
- package/dist/course/course.module.js.map +1 -0
- package/dist/course/course.service.d.ts +215 -0
- package/dist/course/course.service.d.ts.map +1 -0
- package/dist/course/course.service.js +743 -0
- package/dist/course/course.service.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +24 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.js +118 -0
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-session.dto.d.ts +7 -0
- package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-structure-session.dto.js +40 -0
- package/dist/course/dto/create-course-structure-session.dto.js.map +1 -0
- package/dist/course/dto/create-course.dto.d.ts +26 -0
- package/dist/course/dto/create-course.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course.dto.js +138 -0
- package/dist/course/dto/create-course.dto.js.map +1 -0
- package/dist/course/dto/update-course-structure-lesson.dto.d.ts +6 -0
- package/dist/course/dto/update-course-structure-lesson.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-structure-lesson.dto.js +9 -0
- package/dist/course/dto/update-course-structure-lesson.dto.js.map +1 -0
- package/dist/course/dto/update-course-structure-session.dto.d.ts +6 -0
- package/dist/course/dto/update-course-structure-session.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-structure-session.dto.js +9 -0
- package/dist/course/dto/update-course-structure-session.dto.js.map +1 -0
- package/dist/course/dto/update-course.dto.d.ts +6 -0
- package/dist/course/dto/update-course.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course.dto.js +9 -0
- package/dist/course/dto/update-course.dto.js.map +1 -0
- package/dist/dashboard/dashboard.controller.d.ts +101 -0
- package/dist/dashboard/dashboard.controller.d.ts.map +1 -0
- package/dist/dashboard/dashboard.controller.js +40 -0
- package/dist/dashboard/dashboard.controller.js.map +1 -0
- package/dist/dashboard/dashboard.module.d.ts +3 -0
- package/dist/dashboard/dashboard.module.d.ts.map +1 -0
- package/dist/dashboard/dashboard.module.js +25 -0
- package/dist/dashboard/dashboard.module.js.map +1 -0
- package/dist/dashboard/dashboard.service.d.ts +130 -0
- package/dist/dashboard/dashboard.service.d.ts.map +1 -0
- package/dist/dashboard/dashboard.service.js +626 -0
- package/dist/dashboard/dashboard.service.js.map +1 -0
- package/dist/enterprise/dto/add-enterprise-class-group.dto.d.ts +4 -0
- package/dist/enterprise/dto/add-enterprise-class-group.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/add-enterprise-class-group.dto.js +22 -0
- package/dist/enterprise/dto/add-enterprise-class-group.dto.js.map +1 -0
- package/dist/enterprise/dto/add-enterprise-course.dto.d.ts +5 -0
- package/dist/enterprise/dto/add-enterprise-course.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/add-enterprise-course.dto.js +27 -0
- package/dist/enterprise/dto/add-enterprise-course.dto.js.map +1 -0
- package/dist/enterprise/dto/add-enterprise-student.dto.d.ts +5 -0
- package/dist/enterprise/dto/add-enterprise-student.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/add-enterprise-student.dto.js +27 -0
- package/dist/enterprise/dto/add-enterprise-student.dto.js.map +1 -0
- package/dist/enterprise/dto/add-enterprise-user.dto.d.ts +7 -0
- package/dist/enterprise/dto/add-enterprise-user.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/add-enterprise-user.dto.js +36 -0
- package/dist/enterprise/dto/add-enterprise-user.dto.js.map +1 -0
- package/dist/enterprise/dto/create-enterprise.dto.d.ts +10 -0
- package/dist/enterprise/dto/create-enterprise.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/create-enterprise.dto.js +54 -0
- package/dist/enterprise/dto/create-enterprise.dto.js.map +1 -0
- package/dist/enterprise/dto/update-enterprise-student.dto.d.ts +4 -0
- package/dist/enterprise/dto/update-enterprise-student.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/update-enterprise-student.dto.js +22 -0
- package/dist/enterprise/dto/update-enterprise-student.dto.js.map +1 -0
- package/dist/enterprise/dto/update-enterprise-user.dto.d.ts +5 -0
- package/dist/enterprise/dto/update-enterprise-user.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/update-enterprise-user.dto.js +27 -0
- package/dist/enterprise/dto/update-enterprise-user.dto.js.map +1 -0
- package/dist/enterprise/dto/update-enterprise.dto.d.ts +6 -0
- package/dist/enterprise/dto/update-enterprise.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/update-enterprise.dto.js +9 -0
- package/dist/enterprise/dto/update-enterprise.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +269 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -0
- package/dist/enterprise/enterprise.controller.js +311 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -0
- package/dist/enterprise/enterprise.module.d.ts +3 -0
- package/dist/enterprise/enterprise.module.d.ts.map +1 -0
- package/dist/enterprise/enterprise.module.js +25 -0
- package/dist/enterprise/enterprise.module.js.map +1 -0
- package/dist/enterprise/enterprise.service.d.ts +282 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -0
- package/dist/enterprise/enterprise.service.js +627 -0
- package/dist/enterprise/enterprise.service.js.map +1 -0
- package/dist/evaluation/evaluation.controller.d.ts +56 -0
- package/dist/evaluation/evaluation.controller.d.ts.map +1 -0
- package/dist/evaluation/evaluation.controller.js +76 -0
- package/dist/evaluation/evaluation.controller.js.map +1 -0
- package/dist/evaluation/evaluation.module.d.ts +3 -0
- package/dist/evaluation/evaluation.module.d.ts.map +1 -0
- package/dist/evaluation/evaluation.module.js +25 -0
- package/dist/evaluation/evaluation.module.js.map +1 -0
- package/dist/evaluation/evaluation.service.d.ts +67 -0
- package/dist/evaluation/evaluation.service.d.ts.map +1 -0
- package/dist/evaluation/evaluation.service.js +378 -0
- package/dist/evaluation/evaluation.service.js.map +1 -0
- package/dist/exam/dto/create-exam-question.dto.d.ts +25 -0
- package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-exam-question.dto.js +117 -0
- package/dist/exam/dto/create-exam-question.dto.js.map +1 -0
- package/dist/exam/dto/create-exam.dto.d.ts +11 -0
- package/dist/exam/dto/create-exam.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-exam.dto.js +63 -0
- package/dist/exam/dto/create-exam.dto.js.map +1 -0
- package/dist/exam/dto/reorder-exam-questions.dto.d.ts +4 -0
- package/dist/exam/dto/reorder-exam-questions.dto.d.ts.map +1 -0
- package/dist/exam/dto/reorder-exam-questions.dto.js +23 -0
- package/dist/exam/dto/reorder-exam-questions.dto.js.map +1 -0
- package/dist/exam/dto/save-exam-attempt-answers.dto.d.ts +14 -0
- package/dist/exam/dto/save-exam-attempt-answers.dto.d.ts.map +1 -0
- package/dist/exam/dto/save-exam-attempt-answers.dto.js +68 -0
- package/dist/exam/dto/save-exam-attempt-answers.dto.js.map +1 -0
- package/dist/exam/dto/start-exam-attempt.dto.d.ts +4 -0
- package/dist/exam/dto/start-exam-attempt.dto.d.ts.map +1 -0
- package/dist/exam/dto/start-exam-attempt.dto.js +23 -0
- package/dist/exam/dto/start-exam-attempt.dto.js.map +1 -0
- package/dist/exam/dto/submit-exam-attempt.dto.d.ts +5 -0
- package/dist/exam/dto/submit-exam-attempt.dto.d.ts.map +1 -0
- package/dist/exam/dto/submit-exam-attempt.dto.js +23 -0
- package/dist/exam/dto/submit-exam-attempt.dto.js.map +1 -0
- package/dist/exam/dto/update-exam-question.dto.d.ts +6 -0
- package/dist/exam/dto/update-exam-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/update-exam-question.dto.js +9 -0
- package/dist/exam/dto/update-exam-question.dto.js.map +1 -0
- package/dist/exam/dto/update-exam.dto.d.ts +6 -0
- package/dist/exam/dto/update-exam.dto.d.ts.map +1 -0
- package/dist/exam/dto/update-exam.dto.js +9 -0
- package/dist/exam/dto/update-exam.dto.js.map +1 -0
- package/dist/exam/exam-attempt.controller.d.ts +273 -0
- package/dist/exam/exam-attempt.controller.d.ts.map +1 -0
- package/dist/exam/exam-attempt.controller.js +84 -0
- package/dist/exam/exam-attempt.controller.js.map +1 -0
- package/dist/exam/exam-attempt.service.d.ts +302 -0
- package/dist/exam/exam-attempt.service.d.ts.map +1 -0
- package/dist/exam/exam-attempt.service.js +776 -0
- package/dist/exam/exam-attempt.service.js.map +1 -0
- package/dist/exam/exam.controller.d.ts +162 -0
- package/dist/exam/exam.controller.d.ts.map +1 -0
- package/dist/exam/exam.controller.js +158 -0
- package/dist/exam/exam.controller.js.map +1 -0
- package/dist/exam/exam.module.d.ts +3 -0
- package/dist/exam/exam.module.d.ts.map +1 -0
- package/dist/exam/exam.module.js +27 -0
- package/dist/exam/exam.module.js.map +1 -0
- package/dist/exam/exam.service.d.ts +179 -0
- package/dist/exam/exam.service.d.ts.map +1 -0
- package/dist/exam/exam.service.js +597 -0
- package/dist/exam/exam.service.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/instructor/dto/create-instructor.dto.d.ts +10 -0
- package/dist/instructor/dto/create-instructor.dto.d.ts.map +1 -0
- package/dist/instructor/dto/create-instructor.dto.js +55 -0
- package/dist/instructor/dto/create-instructor.dto.js.map +1 -0
- package/dist/instructor/dto/update-instructor.dto.d.ts +9 -0
- package/dist/instructor/dto/update-instructor.dto.d.ts.map +1 -0
- package/dist/instructor/dto/update-instructor.dto.js +51 -0
- package/dist/instructor/dto/update-instructor.dto.js.map +1 -0
- package/dist/instructor/instructor.controller.d.ts +52 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -0
- package/dist/instructor/instructor.controller.js +98 -0
- package/dist/instructor/instructor.controller.js.map +1 -0
- package/dist/instructor/instructor.module.d.ts +3 -0
- package/dist/instructor/instructor.module.d.ts.map +1 -0
- package/dist/instructor/instructor.module.js +25 -0
- package/dist/instructor/instructor.module.js.map +1 -0
- package/dist/instructor/instructor.service.d.ts +79 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -0
- package/dist/instructor/instructor.service.js +528 -0
- package/dist/instructor/instructor.service.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +36 -4
- package/dist/lms.module.js.map +1 -1
- package/dist/reports/reports.controller.d.ts +69 -0
- package/dist/reports/reports.controller.d.ts.map +1 -0
- package/dist/reports/reports.controller.js +40 -0
- package/dist/reports/reports.controller.js.map +1 -0
- package/dist/reports/reports.module.d.ts +3 -0
- package/dist/reports/reports.module.d.ts.map +1 -0
- package/dist/reports/reports.module.js +25 -0
- package/dist/reports/reports.module.js.map +1 -0
- package/dist/reports/reports.service.d.ts +80 -0
- package/dist/reports/reports.service.d.ts.map +1 -0
- package/dist/reports/reports.service.js +366 -0
- package/dist/reports/reports.service.js.map +1 -0
- package/dist/training/dto/create-training.dto.d.ts +19 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -0
- package/dist/training/dto/create-training.dto.js +98 -0
- package/dist/training/dto/create-training.dto.js.map +1 -0
- package/dist/training/dto/update-training.dto.d.ts +6 -0
- package/dist/training/dto/update-training.dto.d.ts.map +1 -0
- package/dist/training/dto/update-training.dto.js +9 -0
- package/dist/training/dto/update-training.dto.js.map +1 -0
- package/dist/training/training.controller.d.ts +195 -0
- package/dist/training/training.controller.d.ts.map +1 -0
- package/dist/training/training.controller.js +104 -0
- package/dist/training/training.controller.js.map +1 -0
- package/dist/training/training.module.d.ts +3 -0
- package/dist/training/training.module.d.ts.map +1 -0
- package/dist/training/training.module.js +25 -0
- package/dist/training/training.module.js.map +1 -0
- package/dist/training/training.service.d.ts +213 -0
- package/dist/training/training.service.d.ts.map +1 -0
- package/dist/training/training.service.js +497 -0
- package/dist/training/training.service.js.map +1 -0
- package/hedhog/data/dashboard.yaml +6 -0
- package/hedhog/data/dashboard_component.yaml +153 -0
- package/hedhog/data/dashboard_component_role.yaml +97 -0
- package/hedhog/data/dashboard_item.yaml +167 -0
- package/hedhog/data/dashboard_role.yaml +6 -0
- package/hedhog/data/instructor_qualification.yaml +16 -0
- package/hedhog/data/menu.yaml +129 -19
- package/hedhog/data/role.yaml +25 -1
- package/hedhog/data/route.yaml +867 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +1992 -0
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +480 -0
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +164 -0
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +120 -0
- package/hedhog/frontend/app/_components/lms-class-calendar.tsx.ejs +272 -0
- package/hedhog/frontend/app/_components/mobile-calendar.tsx.ejs +277 -0
- package/hedhog/frontend/app/_lib/editor/canvasInstance.ts.ejs +48 -0
- package/hedhog/frontend/app/_lib/editor/pctHelpers.ts.ejs +50 -0
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +268 -0
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +94 -0
- package/hedhog/frontend/app/_lib/store/useTemplateStore.ts.ejs +284 -0
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +638 -0
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +916 -0
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +200 -0
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +769 -0
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +104 -0
- package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +354 -0
- package/hedhog/frontend/app/certificates/models/editor/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +883 -0
- package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +279 -0
- package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +1027 -0
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +3130 -993
- package/hedhog/frontend/app/classes/page.tsx.ejs +2731 -759
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +80 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +226 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +71 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +42 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +111 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +215 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +236 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +141 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +57 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +33 -0
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +933 -1103
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +699 -117
- package/hedhog/frontend/app/courses/page.tsx.ejs +1018 -1042
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +317 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-activity-panel.tsx.ejs +88 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +318 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +332 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-class-create-sheet.tsx.ejs +58 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +390 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +112 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +183 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +363 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-constants.ts.ejs +88 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +548 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-utils.ts.ejs +33 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-picker.ts.ejs +31 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-progress-bar.tsx.ejs +21 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-related-tab.tsx.ejs +224 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +397 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +167 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +267 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-system-user-picker.ts.ejs +42 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +96 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +207 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-distribution-chart.tsx.ejs +149 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +596 -0
- package/hedhog/frontend/app/evaluations/page.tsx.ejs +1250 -0
- package/hedhog/frontend/app/exams/[id]/attempt/page.tsx.ejs +642 -196
- package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +1316 -436
- package/hedhog/frontend/app/exams/page.tsx.ejs +799 -546
- package/hedhog/frontend/app/layout.tsx.ejs +5 -0
- package/hedhog/frontend/app/page.tsx.ejs +3 -1220
- package/hedhog/frontend/app/reports/courses/page.tsx.ejs +843 -0
- package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +890 -0
- package/hedhog/frontend/app/reports/page.tsx.ejs +802 -808
- package/hedhog/frontend/app/reports/students/page.tsx.ejs +772 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +1873 -628
- package/hedhog/frontend/messages/en.json +1606 -111
- package/hedhog/frontend/messages/pt.json +1636 -134
- package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +74 -0
- package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +74 -0
- package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +81 -0
- package/hedhog/frontend/widgets/category-distribution-chart.tsx.ejs +119 -0
- package/hedhog/frontend/widgets/class-calendar.tsx.ejs +440 -0
- package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +81 -0
- package/hedhog/frontend/widgets/engagement-chart.tsx.ejs +120 -0
- package/hedhog/frontend/widgets/footer-summary.tsx.ejs +80 -0
- package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +74 -0
- package/hedhog/frontend/widgets/latest-enrollments.tsx.ejs +166 -0
- package/hedhog/frontend/widgets/student-growth-chart.tsx.ejs +89 -0
- package/hedhog/frontend/widgets/top-courses-chart.tsx.ejs +104 -0
- package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +78 -0
- package/hedhog/frontend/widgets/upcoming-classes.tsx.ejs +152 -0
- package/hedhog/table/course.yaml +19 -1
- package/hedhog/table/course_class_group.yaml +8 -0
- package/hedhog/table/course_class_session.yaml +33 -0
- package/hedhog/table/course_instructor.yaml +27 -0
- package/hedhog/table/enterprise.yaml +29 -0
- package/hedhog/table/enterprise_class_group.yaml +20 -0
- package/hedhog/table/enterprise_course.yaml +23 -0
- package/hedhog/table/enterprise_student.yaml +24 -0
- package/hedhog/table/enterprise_user.yaml +35 -0
- package/hedhog/table/instructor_qualification.yaml +26 -0
- package/hedhog/table/instructor_qualification_assignment.yaml +22 -0
- package/hedhog/table/question.yaml +6 -0
- package/package.json +6 -6
- package/src/certificate/certificate.controller.ts +83 -0
- package/src/certificate/certificate.module.ts +13 -0
- package/src/certificate/certificate.service.ts +413 -0
- package/src/certificate/dto/create-certificate-template.dto.ts +25 -0
- package/src/certificate/dto/update-certificate-template.dto.ts +6 -0
- package/src/class-group/class-group.controller.ts +189 -0
- package/src/class-group/class-group.module.ts +12 -0
- package/src/class-group/class-group.service.ts +1802 -0
- package/src/class-group/dto/create-class-group.dto.ts +139 -0
- package/src/class-group/dto/create-session.dto.ts +102 -0
- package/src/class-group/dto/enrollment.dto.ts +70 -0
- package/src/class-group/dto/update-class-group.dto.ts +4 -0
- package/src/class-group/dto/update-session.dto.ts +9 -0
- package/src/course/course-structure.controller.ts +85 -0
- package/src/course/course-structure.service.ts +525 -0
- package/src/course/course.controller.ts +69 -0
- package/src/course/course.module.ts +15 -0
- package/src/course/course.service.ts +920 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +97 -0
- package/src/course/dto/create-course-structure-session.dto.ts +22 -0
- package/src/course/dto/create-course.dto.ts +111 -0
- package/src/course/dto/update-course-structure-lesson.dto.ts +6 -0
- package/src/course/dto/update-course-structure-session.dto.ts +6 -0
- package/src/course/dto/update-course.dto.ts +4 -0
- package/src/dashboard/dashboard.controller.ts +14 -0
- package/src/dashboard/dashboard.module.ts +12 -0
- package/src/dashboard/dashboard.service.ts +726 -0
- package/src/enterprise/dto/add-enterprise-class-group.dto.ts +7 -0
- package/src/enterprise/dto/add-enterprise-course.dto.ts +11 -0
- package/src/enterprise/dto/add-enterprise-student.dto.ts +16 -0
- package/src/enterprise/dto/add-enterprise-user.dto.ts +23 -0
- package/src/enterprise/dto/create-enterprise.dto.ts +41 -0
- package/src/enterprise/dto/update-enterprise-student.dto.ts +7 -0
- package/src/enterprise/dto/update-enterprise-user.dto.ts +11 -0
- package/src/enterprise/dto/update-enterprise.dto.ts +4 -0
- package/src/enterprise/enterprise.controller.ts +233 -0
- package/src/enterprise/enterprise.module.ts +12 -0
- package/src/enterprise/enterprise.service.ts +712 -0
- package/src/evaluation/evaluation.controller.ts +44 -0
- package/src/evaluation/evaluation.module.ts +12 -0
- package/src/evaluation/evaluation.service.ts +394 -0
- package/src/exam/dto/create-exam-question.dto.ts +103 -0
- package/src/exam/dto/create-exam.dto.ts +41 -0
- package/src/exam/dto/reorder-exam-questions.dto.ts +8 -0
- package/src/exam/dto/save-exam-attempt-answers.dto.ts +55 -0
- package/src/exam/dto/start-exam-attempt.dto.ts +8 -0
- package/src/exam/dto/submit-exam-attempt.dto.ts +8 -0
- package/src/exam/dto/update-exam-question.dto.ts +4 -0
- package/src/exam/dto/update-exam.dto.ts +4 -0
- package/src/exam/exam-attempt.controller.ts +65 -0
- package/src/exam/exam-attempt.service.ts +1008 -0
- package/src/exam/exam.controller.ts +102 -0
- package/src/exam/exam.module.ts +14 -0
- package/src/exam/exam.service.ts +784 -0
- package/src/index.ts +29 -0
- package/src/instructor/dto/create-instructor.dto.ts +43 -0
- package/src/instructor/dto/update-instructor.dto.ts +38 -0
- package/src/instructor/instructor.controller.ts +73 -0
- package/src/instructor/instructor.module.ts +12 -0
- package/src/instructor/instructor.service.ts +646 -0
- package/src/lms.module.ts +36 -4
- package/src/reports/reports.controller.ts +14 -0
- package/src/reports/reports.module.ts +12 -0
- package/src/reports/reports.service.ts +485 -0
- package/src/training/dto/create-training.dto.ts +81 -0
- package/src/training/dto/update-training.dto.ts +4 -0
- package/src/training/training.controller.ts +68 -0
- package/src/training/training.module.ts +12 -0
- package/src/training/training.service.ts +574 -0
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
CourseCategoryOption,
|
|
5
|
+
CourseFormSheet,
|
|
6
|
+
CourseSheetFormValues,
|
|
7
|
+
DEFAULT_COURSE_FORM_VALUES,
|
|
8
|
+
getCourseSheetSchema,
|
|
9
|
+
} from '@/app/(app)/(libraries)/lms/_components/course-form-sheet';
|
|
3
10
|
import {
|
|
4
11
|
EmptyState,
|
|
5
12
|
Page,
|
|
6
13
|
PageHeader,
|
|
7
14
|
PaginationFooter,
|
|
15
|
+
SearchBar,
|
|
16
|
+
ViewModeToggle,
|
|
8
17
|
} from '@/components/entity-list';
|
|
9
18
|
import { Badge } from '@/components/ui/badge';
|
|
10
19
|
import { Button } from '@/components/ui/button';
|
|
11
20
|
import { Card, CardContent } from '@/components/ui/card';
|
|
12
|
-
import { Checkbox } from '@/components/ui/checkbox';
|
|
13
21
|
import {
|
|
14
22
|
Dialog,
|
|
15
23
|
DialogContent,
|
|
@@ -27,6 +35,7 @@ import {
|
|
|
27
35
|
} from '@/components/ui/dropdown-menu';
|
|
28
36
|
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
|
29
37
|
import { Input } from '@/components/ui/input';
|
|
38
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
30
39
|
import {
|
|
31
40
|
Select,
|
|
32
41
|
SelectContent,
|
|
@@ -44,47 +53,59 @@ import {
|
|
|
44
53
|
SheetTitle,
|
|
45
54
|
} from '@/components/ui/sheet';
|
|
46
55
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
56
|
+
import { Switch } from '@/components/ui/switch';
|
|
57
|
+
import {
|
|
58
|
+
Table,
|
|
59
|
+
TableBody,
|
|
60
|
+
TableCell,
|
|
61
|
+
TableHead,
|
|
62
|
+
TableHeader,
|
|
63
|
+
TableRow,
|
|
64
|
+
} from '@/components/ui/table';
|
|
47
65
|
import { Textarea } from '@/components/ui/textarea';
|
|
66
|
+
import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
|
|
67
|
+
import {
|
|
68
|
+
DndContext,
|
|
69
|
+
DragEndEvent,
|
|
70
|
+
KeyboardSensor,
|
|
71
|
+
PointerSensor,
|
|
72
|
+
closestCenter,
|
|
73
|
+
useSensor,
|
|
74
|
+
useSensors,
|
|
75
|
+
} from '@dnd-kit/core';
|
|
76
|
+
import {
|
|
77
|
+
SortableContext,
|
|
78
|
+
arrayMove,
|
|
79
|
+
sortableKeyboardCoordinates,
|
|
80
|
+
useSortable,
|
|
81
|
+
verticalListSortingStrategy,
|
|
82
|
+
} from '@dnd-kit/sortable';
|
|
83
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
84
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
48
85
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
49
86
|
import { motion } from 'framer-motion';
|
|
50
87
|
import {
|
|
51
88
|
AlertTriangle,
|
|
52
|
-
BarChart3,
|
|
53
|
-
BookOpen,
|
|
54
89
|
Clock,
|
|
55
|
-
Eye,
|
|
56
|
-
FileCheck,
|
|
57
90
|
GraduationCap,
|
|
91
|
+
GripVertical,
|
|
58
92
|
Layers,
|
|
59
|
-
LayoutDashboard,
|
|
60
93
|
Loader2,
|
|
61
94
|
MoreHorizontal,
|
|
62
95
|
Pencil,
|
|
63
96
|
Plus,
|
|
64
|
-
Search,
|
|
65
97
|
Target,
|
|
66
98
|
Trash2,
|
|
67
99
|
Users,
|
|
68
100
|
X,
|
|
69
101
|
} from 'lucide-react';
|
|
70
102
|
import { useTranslations } from 'next-intl';
|
|
71
|
-
import {
|
|
103
|
+
import { useRouter } from 'next/navigation';
|
|
72
104
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
73
|
-
import { Controller, useForm } from 'react-hook-form';
|
|
105
|
+
import { Controller, useForm, useWatch } from 'react-hook-form';
|
|
74
106
|
import { toast } from 'sonner';
|
|
75
107
|
import { z } from 'zod';
|
|
76
108
|
|
|
77
|
-
// ── Nav ───────────────────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
const NAV_ITEMS = [
|
|
80
|
-
{ label: 'Dashboard', href: '/', icon: LayoutDashboard },
|
|
81
|
-
{ label: 'Cursos', href: '/cursos', icon: BookOpen },
|
|
82
|
-
{ label: 'Turmas', href: '/turmas', icon: Users },
|
|
83
|
-
{ label: 'Exames', href: '/exames', icon: FileCheck },
|
|
84
|
-
{ label: 'Formacoes', href: '/formacoes', icon: GraduationCap },
|
|
85
|
-
{ label: 'Relatorios', href: '/relatorios', icon: BarChart3 },
|
|
86
|
-
];
|
|
87
|
-
|
|
88
109
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
89
110
|
|
|
90
111
|
interface Formacao {
|
|
@@ -95,10 +116,300 @@ interface Formacao {
|
|
|
95
116
|
nivel: string;
|
|
96
117
|
prerequisitos: string;
|
|
97
118
|
cursos: string[];
|
|
119
|
+
exams?: string[];
|
|
120
|
+
courseIds: number[];
|
|
121
|
+
examIds?: number[];
|
|
122
|
+
items?: LearningPathItem[];
|
|
98
123
|
cargaTotal: number;
|
|
99
124
|
alunos: number;
|
|
100
125
|
status: 'ativa' | 'rascunho' | 'encerrada';
|
|
101
126
|
criadoEm: string;
|
|
127
|
+
primaryColor?: string;
|
|
128
|
+
secondaryColor?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type TrainingColorPayload = {
|
|
132
|
+
primaryColor?: string | null;
|
|
133
|
+
secondaryColor?: string | null;
|
|
134
|
+
primary_color?: string | null;
|
|
135
|
+
secondary_color?: string | null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
interface CursoOption {
|
|
139
|
+
id: number;
|
|
140
|
+
nome: string;
|
|
141
|
+
cargaHoraria: number;
|
|
142
|
+
categories: string[];
|
|
143
|
+
area: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface ExameOption {
|
|
147
|
+
id: number;
|
|
148
|
+
titulo: string;
|
|
149
|
+
limiteTempo: number;
|
|
150
|
+
status: 'publicado' | 'rascunho' | 'encerrado';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface LearningPathItem {
|
|
154
|
+
id?: number;
|
|
155
|
+
type: 'course' | 'exam';
|
|
156
|
+
itemId: number;
|
|
157
|
+
order: number;
|
|
158
|
+
isRequired?: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface TrailRenderableItem {
|
|
162
|
+
uid: string;
|
|
163
|
+
type: 'course' | 'exam';
|
|
164
|
+
itemId: number;
|
|
165
|
+
title: string;
|
|
166
|
+
subtitle: string;
|
|
167
|
+
order: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type ApiTrainingListResponse = {
|
|
171
|
+
total: number;
|
|
172
|
+
page: number;
|
|
173
|
+
pageSize: number;
|
|
174
|
+
lastPage: number;
|
|
175
|
+
data: Formacao[];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type ApiTrainingStatsResponse = {
|
|
179
|
+
totalTraining: number;
|
|
180
|
+
activeTraining: number;
|
|
181
|
+
enrolledStudents: number;
|
|
182
|
+
coveredCourses: number;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
type ApiCourseListResponse = {
|
|
186
|
+
data: Array<{
|
|
187
|
+
id: number;
|
|
188
|
+
title: string;
|
|
189
|
+
durationHours: number;
|
|
190
|
+
categories?: string[];
|
|
191
|
+
}>;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
type ApiExamListResponse = {
|
|
195
|
+
data: Array<{
|
|
196
|
+
id: number;
|
|
197
|
+
title: string;
|
|
198
|
+
timeLimit: number;
|
|
199
|
+
status: 'published' | 'draft' | 'closed' | 'archived';
|
|
200
|
+
}>;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
type ApiCategory = {
|
|
204
|
+
id: number;
|
|
205
|
+
slug: string;
|
|
206
|
+
name: string;
|
|
207
|
+
status?: 'active' | 'inactive';
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
type ApiCategoryList = {
|
|
211
|
+
data: ApiCategory[];
|
|
212
|
+
total: number;
|
|
213
|
+
page: number;
|
|
214
|
+
pageSize: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
type ViewMode = 'cards' | 'list';
|
|
218
|
+
|
|
219
|
+
type Locale = {
|
|
220
|
+
id?: number;
|
|
221
|
+
code: string;
|
|
222
|
+
name: string;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const createExamQuickSchema = z.object({
|
|
226
|
+
titulo: z.string().min(3, 'Minimo 3 caracteres'),
|
|
227
|
+
notaMinima: z.coerce.number().min(0).max(10),
|
|
228
|
+
limiteTempo: z.coerce.number().min(1),
|
|
229
|
+
shuffle: z.boolean().default(false),
|
|
230
|
+
status: z.enum(['rascunho', 'publicado', 'encerrado']),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
type ExamQuickForm = z.infer<typeof createExamQuickSchema>;
|
|
234
|
+
|
|
235
|
+
function normalizeText(value: string) {
|
|
236
|
+
return value
|
|
237
|
+
.trim()
|
|
238
|
+
.toLowerCase()
|
|
239
|
+
.normalize('NFD')
|
|
240
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function categorySlugToArea(slugs: string[] = []) {
|
|
244
|
+
const normalized = slugs.map((slug) => normalizeText(slug));
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
normalized.some(
|
|
248
|
+
(slug) =>
|
|
249
|
+
slug === 'design' ||
|
|
250
|
+
slug.includes('design') ||
|
|
251
|
+
slug.includes('ux') ||
|
|
252
|
+
slug.includes('ui')
|
|
253
|
+
)
|
|
254
|
+
) {
|
|
255
|
+
return 'Design';
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
normalized.some(
|
|
259
|
+
(slug) =>
|
|
260
|
+
slug === 'gestao' ||
|
|
261
|
+
slug === 'management' ||
|
|
262
|
+
slug.includes('gestao') ||
|
|
263
|
+
slug.includes('management')
|
|
264
|
+
)
|
|
265
|
+
) {
|
|
266
|
+
return 'Gestao';
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
normalized.some(
|
|
270
|
+
(slug) => slug === 'marketing' || slug.includes('marketing')
|
|
271
|
+
)
|
|
272
|
+
) {
|
|
273
|
+
return 'Marketing';
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
normalized.some(
|
|
277
|
+
(slug) =>
|
|
278
|
+
slug === 'financas' ||
|
|
279
|
+
slug === 'finance' ||
|
|
280
|
+
slug.includes('financ') ||
|
|
281
|
+
slug.includes('accounting')
|
|
282
|
+
)
|
|
283
|
+
) {
|
|
284
|
+
return 'Financas';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return 'Tecnologia';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeAreaValue(value: string) {
|
|
291
|
+
const normalized = normalizeText(value);
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
normalized === 'tecnologia' ||
|
|
295
|
+
normalized === 'technology' ||
|
|
296
|
+
normalized.includes('tecnolog') ||
|
|
297
|
+
normalized.includes('technology')
|
|
298
|
+
) {
|
|
299
|
+
return 'Tecnologia';
|
|
300
|
+
}
|
|
301
|
+
if (normalized === 'design' || normalized.includes('design')) return 'Design';
|
|
302
|
+
if (
|
|
303
|
+
normalized === 'gestao' ||
|
|
304
|
+
normalized === 'management' ||
|
|
305
|
+
normalized.includes('gestao') ||
|
|
306
|
+
normalized.includes('management')
|
|
307
|
+
) {
|
|
308
|
+
return 'Gestao';
|
|
309
|
+
}
|
|
310
|
+
if (normalized === 'marketing' || normalized.includes('marketing')) {
|
|
311
|
+
return 'Marketing';
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
normalized === 'financas' ||
|
|
315
|
+
normalized === 'finance' ||
|
|
316
|
+
normalized.includes('financ') ||
|
|
317
|
+
normalized.includes('accounting')
|
|
318
|
+
) {
|
|
319
|
+
return 'Financas';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return 'Tecnologia';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeLevelValue(value: string) {
|
|
326
|
+
const normalized = normalizeText(value);
|
|
327
|
+
|
|
328
|
+
if (['iniciante', 'beginner'].includes(normalized)) return 'Iniciante';
|
|
329
|
+
if (['intermediario', 'intermediate'].includes(normalized)) {
|
|
330
|
+
return 'Intermediario';
|
|
331
|
+
}
|
|
332
|
+
if (['avancado', 'advanced'].includes(normalized)) return 'Avancado';
|
|
333
|
+
|
|
334
|
+
return 'Iniciante';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function normalizeStatusValue(value: string) {
|
|
338
|
+
const normalized = normalizeText(value);
|
|
339
|
+
|
|
340
|
+
if (['ativa', 'active'].includes(normalized)) return 'ativa';
|
|
341
|
+
if (['encerrada', 'archived'].includes(normalized)) return 'encerrada';
|
|
342
|
+
|
|
343
|
+
return 'rascunho';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function slugifyText(value: string) {
|
|
347
|
+
return normalizeText(value)
|
|
348
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
349
|
+
.replace(/\s+/g, '-')
|
|
350
|
+
.replace(/-+/g, '-')
|
|
351
|
+
.replace(/^-|-$/g, '')
|
|
352
|
+
.slice(0, 64);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeHexColor(value?: string | null) {
|
|
356
|
+
if (!value) return null;
|
|
357
|
+
|
|
358
|
+
const raw = value.trim();
|
|
359
|
+
if (!raw) return null;
|
|
360
|
+
|
|
361
|
+
const prefixed = raw.startsWith('#') ? raw : `#${raw}`;
|
|
362
|
+
|
|
363
|
+
if (/^#([0-9A-Fa-f]{6})$/.test(prefixed)) return prefixed;
|
|
364
|
+
if (/^#([0-9A-Fa-f]{3})$/.test(prefixed)) {
|
|
365
|
+
const hex = prefixed.slice(1);
|
|
366
|
+
|
|
367
|
+
return `#${hex
|
|
368
|
+
.split('')
|
|
369
|
+
.map((char) => `${char}${char}`)
|
|
370
|
+
.join('')}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function normalizeTrainingColorPayload<T extends TrainingColorPayload>(
|
|
377
|
+
payload: T
|
|
378
|
+
) {
|
|
379
|
+
const primaryColor = normalizeHexColor(
|
|
380
|
+
payload.primaryColor ?? payload.primary_color
|
|
381
|
+
);
|
|
382
|
+
const secondaryColor = normalizeHexColor(
|
|
383
|
+
payload.secondaryColor ?? payload.secondary_color
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
...payload,
|
|
388
|
+
primaryColor: primaryColor ?? undefined,
|
|
389
|
+
secondaryColor: secondaryColor ?? undefined,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function normalizeTrainingListPayload(
|
|
394
|
+
payload: ApiTrainingListResponse
|
|
395
|
+
): ApiTrainingListResponse {
|
|
396
|
+
return {
|
|
397
|
+
...payload,
|
|
398
|
+
data: (payload.data ?? []).map((item) =>
|
|
399
|
+
normalizeTrainingColorPayload(item as Formacao & TrainingColorPayload)
|
|
400
|
+
),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildCourseCodeFromTitle(title: string) {
|
|
405
|
+
const base = slugifyText(title)
|
|
406
|
+
.toUpperCase()
|
|
407
|
+
.replace(/[^A-Z0-9]/g, '-');
|
|
408
|
+
const compact = base.replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
409
|
+
|
|
410
|
+
if (compact.length >= 2) return compact.slice(0, 32);
|
|
411
|
+
|
|
412
|
+
return `COURSE-${Date.now().toString().slice(-6)}`;
|
|
102
413
|
}
|
|
103
414
|
|
|
104
415
|
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
@@ -106,10 +417,18 @@ interface Formacao {
|
|
|
106
417
|
const formacaoSchema = z.object({
|
|
107
418
|
nome: z.string().min(3, 'Minimo 3 caracteres'),
|
|
108
419
|
descricao: z.string().min(10, 'Minimo 10 caracteres'),
|
|
109
|
-
area: z.
|
|
110
|
-
nivel: z.
|
|
420
|
+
area: z.enum(['Tecnologia', 'Design', 'Gestao', 'Marketing', 'Financas']),
|
|
421
|
+
nivel: z.enum(['Iniciante', 'Intermediario', 'Avancado']),
|
|
111
422
|
prerequisitos: z.string().optional(),
|
|
112
|
-
status: z.
|
|
423
|
+
status: z.enum(['rascunho', 'ativa', 'encerrada']),
|
|
424
|
+
primaryColor: z
|
|
425
|
+
.string()
|
|
426
|
+
.regex(/^#([0-9A-Fa-f]{6})$/, 'Cor primária inválida')
|
|
427
|
+
.default('#1D4ED8'),
|
|
428
|
+
secondaryColor: z
|
|
429
|
+
.string()
|
|
430
|
+
.regex(/^#([0-9A-Fa-f]{6})$/, 'Cor secundária inválida')
|
|
431
|
+
.default('#111827'),
|
|
113
432
|
});
|
|
114
433
|
|
|
115
434
|
type FormacaoForm = z.infer<typeof formacaoSchema>;
|
|
@@ -125,155 +444,8 @@ const STATUS_MAP: Record<
|
|
|
125
444
|
encerrada: { label: 'Encerrada', variant: 'outline' },
|
|
126
445
|
};
|
|
127
446
|
|
|
128
|
-
const AREA_COLORS: Record<string, string> = {
|
|
129
|
-
Tecnologia: 'bg-blue-50 text-blue-700 border-blue-200',
|
|
130
|
-
Design: 'bg-purple-50 text-purple-700 border-purple-200',
|
|
131
|
-
Gestao: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
132
|
-
Marketing: 'bg-orange-50 text-orange-700 border-orange-200',
|
|
133
|
-
Financas: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
|
134
|
-
};
|
|
135
|
-
|
|
136
447
|
const PAGE_SIZES = [6, 12, 24];
|
|
137
|
-
|
|
138
|
-
const availableCursos = [
|
|
139
|
-
{ id: 'react', nome: 'React Avancado', cargaHoraria: 60 },
|
|
140
|
-
{ id: 'ux', nome: 'UX Design Fundamentals', cargaHoraria: 40 },
|
|
141
|
-
{ id: 'python', nome: 'Python para Data Science', cargaHoraria: 80 },
|
|
142
|
-
{ id: 'node', nome: 'Node.js Completo', cargaHoraria: 70 },
|
|
143
|
-
{ id: 'typescript', nome: 'TypeScript na Pratica', cargaHoraria: 50 },
|
|
144
|
-
{ id: 'figma', nome: 'Figma para Iniciantes', cargaHoraria: 25 },
|
|
145
|
-
{ id: 'agile', nome: 'Gestao de Projetos Ageis', cargaHoraria: 30 },
|
|
146
|
-
{ id: 'marketing', nome: 'Marketing Digital', cargaHoraria: 45 },
|
|
147
|
-
{ id: 'design-system', nome: 'Design System', cargaHoraria: 35 },
|
|
148
|
-
{ id: 'excel', nome: 'Excel para Negocios', cargaHoraria: 30 },
|
|
149
|
-
{ id: 'lideranca', nome: 'Lideranca e Comunicacao', cargaHoraria: 20 },
|
|
150
|
-
{ id: 'seo', nome: 'SEO Avancado', cargaHoraria: 35 },
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
// ── Seed Data ─────────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
const initialFormacoes: Formacao[] = [
|
|
156
|
-
{
|
|
157
|
-
id: 1,
|
|
158
|
-
nome: 'Full Stack Developer',
|
|
159
|
-
descricao:
|
|
160
|
-
'Formacao completa para desenvolvimento full stack com React, Node.js e TypeScript.',
|
|
161
|
-
area: 'Tecnologia',
|
|
162
|
-
nivel: 'Avancado',
|
|
163
|
-
prerequisitos: 'JavaScript basico',
|
|
164
|
-
cursos: ['React Avancado', 'Node.js Completo', 'TypeScript na Pratica'],
|
|
165
|
-
cargaTotal: 180,
|
|
166
|
-
alunos: 342,
|
|
167
|
-
status: 'ativa',
|
|
168
|
-
criadoEm: '2024-01-01',
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
id: 2,
|
|
172
|
-
nome: 'UX/UI Designer Profissional',
|
|
173
|
-
descricao:
|
|
174
|
-
'Torne-se um designer completo com habilidades em UX, UI e design systems.',
|
|
175
|
-
area: 'Design',
|
|
176
|
-
nivel: 'Intermediario',
|
|
177
|
-
prerequisitos: 'Nenhum',
|
|
178
|
-
cursos: [
|
|
179
|
-
'UX Design Fundamentals',
|
|
180
|
-
'Figma para Iniciantes',
|
|
181
|
-
'Design System',
|
|
182
|
-
],
|
|
183
|
-
cargaTotal: 100,
|
|
184
|
-
alunos: 198,
|
|
185
|
-
status: 'ativa',
|
|
186
|
-
criadoEm: '2024-01-15',
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
id: 3,
|
|
190
|
-
nome: 'Data Science com Python',
|
|
191
|
-
descricao:
|
|
192
|
-
'Domine ciencia de dados desde fundamentos ate machine learning com Python.',
|
|
193
|
-
area: 'Tecnologia',
|
|
194
|
-
nivel: 'Intermediario',
|
|
195
|
-
prerequisitos: 'Logica de programacao',
|
|
196
|
-
cursos: ['Python para Data Science', 'Excel para Negocios'],
|
|
197
|
-
cargaTotal: 110,
|
|
198
|
-
alunos: 267,
|
|
199
|
-
status: 'ativa',
|
|
200
|
-
criadoEm: '2024-02-01',
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
id: 4,
|
|
204
|
-
nome: 'Gestao e Lideranca',
|
|
205
|
-
descricao:
|
|
206
|
-
'Formacao em gestao de projetos ageis e habilidades de lideranca para times modernos.',
|
|
207
|
-
area: 'Gestao',
|
|
208
|
-
nivel: 'Iniciante',
|
|
209
|
-
prerequisitos: 'Nenhum',
|
|
210
|
-
cursos: [
|
|
211
|
-
'Gestao de Projetos Ageis',
|
|
212
|
-
'Lideranca e Comunicacao',
|
|
213
|
-
'Excel para Negocios',
|
|
214
|
-
],
|
|
215
|
-
cargaTotal: 80,
|
|
216
|
-
alunos: 156,
|
|
217
|
-
status: 'ativa',
|
|
218
|
-
criadoEm: '2024-02-15',
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
id: 5,
|
|
222
|
-
nome: 'Marketing Digital Completo',
|
|
223
|
-
descricao:
|
|
224
|
-
'Domine estrategias de marketing digital, SEO e conteudo para web.',
|
|
225
|
-
area: 'Marketing',
|
|
226
|
-
nivel: 'Intermediario',
|
|
227
|
-
prerequisitos: 'Nenhum',
|
|
228
|
-
cursos: ['Marketing Digital', 'SEO Avancado'],
|
|
229
|
-
cargaTotal: 80,
|
|
230
|
-
alunos: 0,
|
|
231
|
-
status: 'rascunho',
|
|
232
|
-
criadoEm: '2024-03-01',
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: 6,
|
|
236
|
-
nome: 'Frontend Developer',
|
|
237
|
-
descricao:
|
|
238
|
-
'Especializacao em desenvolvimento frontend com as tecnologias mais atuais do mercado.',
|
|
239
|
-
area: 'Tecnologia',
|
|
240
|
-
nivel: 'Intermediario',
|
|
241
|
-
prerequisitos: 'HTML, CSS e JS basico',
|
|
242
|
-
cursos: ['React Avancado', 'TypeScript na Pratica', 'Design System'],
|
|
243
|
-
cargaTotal: 145,
|
|
244
|
-
alunos: 89,
|
|
245
|
-
status: 'ativa',
|
|
246
|
-
criadoEm: '2024-03-15',
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
id: 7,
|
|
250
|
-
nome: 'Design Thinking e Inovacao',
|
|
251
|
-
descricao:
|
|
252
|
-
'Aprenda metodologias de design thinking e processos de inovacao para negócios.',
|
|
253
|
-
area: 'Design',
|
|
254
|
-
nivel: 'Iniciante',
|
|
255
|
-
prerequisitos: 'Nenhum',
|
|
256
|
-
cursos: ['UX Design Fundamentals', 'Gestao de Projetos Ageis'],
|
|
257
|
-
cargaTotal: 70,
|
|
258
|
-
alunos: 412,
|
|
259
|
-
status: 'encerrada',
|
|
260
|
-
criadoEm: '2023-09-01',
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
id: 8,
|
|
264
|
-
nome: 'Analista de Dados',
|
|
265
|
-
descricao:
|
|
266
|
-
'Formacao completa para analise de dados empresariais com ferramentas modernas.',
|
|
267
|
-
area: 'Tecnologia',
|
|
268
|
-
nivel: 'Iniciante',
|
|
269
|
-
prerequisitos: 'Nenhum',
|
|
270
|
-
cursos: ['Excel para Negocios', 'Python para Data Science'],
|
|
271
|
-
cargaTotal: 110,
|
|
272
|
-
alunos: 203,
|
|
273
|
-
status: 'ativa',
|
|
274
|
-
criadoEm: '2024-04-01',
|
|
275
|
-
},
|
|
276
|
-
];
|
|
448
|
+
const API_TRAINING_CACHE_KEY = 'lms:training:api-cache';
|
|
277
449
|
|
|
278
450
|
// ── Animations ────────────────────────────────────────────────────────────────
|
|
279
451
|
|
|
@@ -290,34 +462,101 @@ const stagger = {
|
|
|
290
462
|
show: { transition: { staggerChildren: 0.05 } },
|
|
291
463
|
};
|
|
292
464
|
|
|
465
|
+
function SortableTrailItem(props: {
|
|
466
|
+
item: TrailRenderableItem;
|
|
467
|
+
onRemove: (uid: string) => void;
|
|
468
|
+
}) {
|
|
469
|
+
const { item, onRemove } = props;
|
|
470
|
+
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
471
|
+
useSortable({ id: item.uid });
|
|
472
|
+
|
|
473
|
+
const style = {
|
|
474
|
+
transform: CSS.Transform.toString(transform),
|
|
475
|
+
transition,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div
|
|
480
|
+
ref={setNodeRef}
|
|
481
|
+
style={style}
|
|
482
|
+
className="flex items-center gap-2 border-b p-2.5 last:border-0"
|
|
483
|
+
>
|
|
484
|
+
<Button
|
|
485
|
+
type="button"
|
|
486
|
+
variant="ghost"
|
|
487
|
+
size="icon"
|
|
488
|
+
className="size-7 text-muted-foreground"
|
|
489
|
+
aria-label="Arrastar item"
|
|
490
|
+
{...attributes}
|
|
491
|
+
{...listeners}
|
|
492
|
+
>
|
|
493
|
+
<GripVertical className="size-4" />
|
|
494
|
+
</Button>
|
|
495
|
+
|
|
496
|
+
<div className="min-w-0 flex-1">
|
|
497
|
+
<p className="truncate text-sm font-medium">{item.title}</p>
|
|
498
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
499
|
+
{item.subtitle}
|
|
500
|
+
</p>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<Badge variant="outline" className="text-[10px] uppercase tracking-wide">
|
|
504
|
+
{item.type === 'course' ? 'Curso' : 'Exame'}
|
|
505
|
+
</Badge>
|
|
506
|
+
|
|
507
|
+
<Button
|
|
508
|
+
type="button"
|
|
509
|
+
variant="ghost"
|
|
510
|
+
size="sm"
|
|
511
|
+
onClick={() => onRemove(item.uid)}
|
|
512
|
+
className="h-7 px-2 text-muted-foreground"
|
|
513
|
+
>
|
|
514
|
+
<X className="size-3.5" />
|
|
515
|
+
</Button>
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
293
520
|
// ── Page ──────────────────────────────────────────────────────────────────────
|
|
294
521
|
|
|
295
522
|
export default function TrainingPage() {
|
|
296
523
|
const t = useTranslations('lms.TrainingPage');
|
|
297
|
-
const
|
|
524
|
+
const tCourse = useTranslations('lms.CoursesPage');
|
|
525
|
+
const tExam = useTranslations('lms.ExamsPage');
|
|
298
526
|
const router = useRouter();
|
|
527
|
+
const { request } = useApp();
|
|
299
528
|
|
|
300
|
-
const [loading, setLoading] = useState(true);
|
|
301
|
-
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
302
|
-
const [formacoes, setFormacoes] = useState<Formacao[]>(initialFormacoes);
|
|
303
529
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
304
530
|
const [editingFormacao, setEditingFormacao] = useState<Formacao | null>(null);
|
|
305
531
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
306
532
|
const [formacaoToDelete, setFormacaoToDelete] = useState<Formacao | null>(
|
|
307
533
|
null
|
|
308
534
|
);
|
|
309
|
-
const [
|
|
535
|
+
const [learningPathItems, setLearningPathItems] = useState<
|
|
536
|
+
LearningPathItem[]
|
|
537
|
+
>([]);
|
|
538
|
+
const [selectedCourseToAdd, setSelectedCourseToAdd] = useState('');
|
|
539
|
+
const [selectedExamToAdd, setSelectedExamToAdd] = useState('');
|
|
310
540
|
const [saving, setSaving] = useState(false);
|
|
541
|
+
const [loadingEditSheet, setLoadingEditSheet] = useState(false);
|
|
542
|
+
const [courseSheetOpen, setCourseSheetOpen] = useState(false);
|
|
543
|
+
const [examSheetOpen, setExamSheetOpen] = useState(false);
|
|
544
|
+
const [creatingCourse, setCreatingCourse] = useState(false);
|
|
545
|
+
const [creatingExam, setCreatingExam] = useState(false);
|
|
546
|
+
const [cachedListData, setCachedListData] =
|
|
547
|
+
useState<ApiTrainingListResponse | null>(null);
|
|
548
|
+
const initialLearningPathRef = useRef<LearningPathItem[]>([]);
|
|
311
549
|
|
|
312
550
|
// Search/filter inputs
|
|
313
551
|
const [buscaInput, setBuscaInput] = useState('');
|
|
314
|
-
const [
|
|
552
|
+
const [buscaDebounced, setBuscaDebounced] = useState('');
|
|
553
|
+
const [filtroStatusInput, setFiltroStatusInput] = useState('todos');
|
|
315
554
|
const [filtroNivelInput, setFiltroNivelInput] = useState('todos');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
555
|
+
const [viewMode, setViewMode] = usePersistedViewMode<ViewMode>({
|
|
556
|
+
storageKey: 'lms:training:view-mode',
|
|
557
|
+
defaultValue: 'cards',
|
|
558
|
+
allowedValues: ['cards', 'list'],
|
|
559
|
+
});
|
|
321
560
|
|
|
322
561
|
// Pagination
|
|
323
562
|
const [currentPage, setCurrentPage] = useState(1);
|
|
@@ -328,72 +567,367 @@ export default function TrainingPage() {
|
|
|
328
567
|
new Map()
|
|
329
568
|
);
|
|
330
569
|
|
|
570
|
+
const sensors = useSensors(
|
|
571
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
572
|
+
useSensor(KeyboardSensor, {
|
|
573
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const {
|
|
578
|
+
data: listData,
|
|
579
|
+
isLoading: isLoadingList,
|
|
580
|
+
isFetching: isFetchingList,
|
|
581
|
+
refetch: refetchTraining,
|
|
582
|
+
} = useQuery<ApiTrainingListResponse>({
|
|
583
|
+
queryKey: [
|
|
584
|
+
'lms-training-list',
|
|
585
|
+
currentPage,
|
|
586
|
+
pageSize,
|
|
587
|
+
buscaDebounced,
|
|
588
|
+
filtroStatusInput,
|
|
589
|
+
filtroNivelInput,
|
|
590
|
+
],
|
|
591
|
+
queryFn: async () => {
|
|
592
|
+
const response = await request<ApiTrainingListResponse>({
|
|
593
|
+
url: '/lms/training',
|
|
594
|
+
method: 'GET',
|
|
595
|
+
params: {
|
|
596
|
+
page: currentPage,
|
|
597
|
+
pageSize,
|
|
598
|
+
search: buscaDebounced || undefined,
|
|
599
|
+
status:
|
|
600
|
+
filtroStatusInput !== 'todos'
|
|
601
|
+
? ptStatusToApi(filtroStatusInput as Formacao['status'])
|
|
602
|
+
: undefined,
|
|
603
|
+
level: filtroNivelInput !== 'todos' ? filtroNivelInput : undefined,
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
return normalizeTrainingListPayload(response.data);
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const { data: statsData, refetch: refetchStats } =
|
|
612
|
+
useQuery<ApiTrainingStatsResponse>({
|
|
613
|
+
queryKey: ['lms-training-stats'],
|
|
614
|
+
queryFn: async () => {
|
|
615
|
+
const response = await request<ApiTrainingStatsResponse>({
|
|
616
|
+
url: '/lms/training/stats',
|
|
617
|
+
method: 'GET',
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return response.data;
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const {
|
|
625
|
+
data: coursesData,
|
|
626
|
+
refetch: refetchCourses,
|
|
627
|
+
isFetching: isFetchingCourses,
|
|
628
|
+
} = useQuery<ApiCourseListResponse>({
|
|
629
|
+
queryKey: ['lms-training-course-options'],
|
|
630
|
+
queryFn: async () => {
|
|
631
|
+
const response = await request<ApiCourseListResponse>({
|
|
632
|
+
url: '/lms/courses',
|
|
633
|
+
method: 'GET',
|
|
634
|
+
params: {
|
|
635
|
+
page: 1,
|
|
636
|
+
pageSize: 1000,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
return response.data;
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const {
|
|
645
|
+
data: examsData,
|
|
646
|
+
refetch: refetchExams,
|
|
647
|
+
isFetching: isFetchingExams,
|
|
648
|
+
} = useQuery<ApiExamListResponse>({
|
|
649
|
+
queryKey: ['lms-training-exam-options'],
|
|
650
|
+
queryFn: async () => {
|
|
651
|
+
const response = await request<ApiExamListResponse>({
|
|
652
|
+
url: '/lms/exams',
|
|
653
|
+
method: 'GET',
|
|
654
|
+
params: {
|
|
655
|
+
page: 1,
|
|
656
|
+
pageSize: 1000,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
return response.data;
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const { data: categoryListData, refetch: refetchCategories } =
|
|
665
|
+
useQuery<ApiCategoryList>({
|
|
666
|
+
queryKey: ['lms-training-category-options'],
|
|
667
|
+
queryFn: async () => {
|
|
668
|
+
const response = await request<ApiCategoryList>({
|
|
669
|
+
url: '/category',
|
|
670
|
+
method: 'GET',
|
|
671
|
+
params: {
|
|
672
|
+
page: 1,
|
|
673
|
+
pageSize: 500,
|
|
674
|
+
status: 'all',
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const payload = response.data as ApiCategoryList | ApiCategory[];
|
|
679
|
+
if (Array.isArray(payload)) {
|
|
680
|
+
return {
|
|
681
|
+
data: payload,
|
|
682
|
+
total: payload.length,
|
|
683
|
+
page: 1,
|
|
684
|
+
pageSize: payload.length,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return payload;
|
|
689
|
+
},
|
|
690
|
+
initialData: {
|
|
691
|
+
data: [],
|
|
692
|
+
total: 0,
|
|
693
|
+
page: 1,
|
|
694
|
+
pageSize: 500,
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
if (courseSheetOpen) {
|
|
700
|
+
void refetchCategories();
|
|
701
|
+
}
|
|
702
|
+
}, [courseSheetOpen, refetchCategories]);
|
|
703
|
+
|
|
704
|
+
const availableCursos: CursoOption[] = (coursesData?.data ?? []).map(
|
|
705
|
+
(course) => ({
|
|
706
|
+
id: course.id,
|
|
707
|
+
nome: course.title,
|
|
708
|
+
cargaHoraria: course.durationHours ?? 0,
|
|
709
|
+
categories: course.categories ?? [],
|
|
710
|
+
area: categorySlugToArea(course.categories ?? []),
|
|
711
|
+
})
|
|
712
|
+
);
|
|
713
|
+
|
|
331
714
|
const form = useForm<FormacaoForm>({
|
|
332
715
|
resolver: zodResolver(formacaoSchema),
|
|
333
716
|
defaultValues: {
|
|
334
717
|
nome: '',
|
|
335
718
|
descricao: '',
|
|
336
|
-
area: '',
|
|
337
|
-
nivel: '',
|
|
719
|
+
area: 'Tecnologia',
|
|
720
|
+
nivel: 'Iniciante',
|
|
338
721
|
prerequisitos: '',
|
|
339
722
|
status: 'rascunho',
|
|
723
|
+
primaryColor: '#1D4ED8',
|
|
724
|
+
secondaryColor: '#111827',
|
|
340
725
|
},
|
|
341
726
|
});
|
|
342
727
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
728
|
+
const courseForm = useForm<CourseSheetFormValues>({
|
|
729
|
+
resolver: zodResolver(getCourseSheetSchema(tCourse)),
|
|
730
|
+
defaultValues: DEFAULT_COURSE_FORM_VALUES,
|
|
731
|
+
});
|
|
732
|
+
const examForm = useForm<ExamQuickForm>({
|
|
733
|
+
resolver: zodResolver(createExamQuickSchema),
|
|
734
|
+
defaultValues: {
|
|
735
|
+
titulo: '',
|
|
736
|
+
notaMinima: 7,
|
|
737
|
+
limiteTempo: 60,
|
|
738
|
+
shuffle: false,
|
|
739
|
+
status: 'rascunho',
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
const watchedFormValues = useWatch({ control: form.control });
|
|
347
743
|
|
|
348
|
-
|
|
744
|
+
const availableExams: ExameOption[] = (examsData?.data ?? []).map((exam) => ({
|
|
745
|
+
id: exam.id,
|
|
746
|
+
titulo: exam.title,
|
|
747
|
+
limiteTempo: exam.timeLimit ?? 0,
|
|
748
|
+
status:
|
|
749
|
+
exam.status === 'published'
|
|
750
|
+
? 'publicado'
|
|
751
|
+
: exam.status === 'draft'
|
|
752
|
+
? 'rascunho'
|
|
753
|
+
: 'encerrado',
|
|
754
|
+
}));
|
|
349
755
|
|
|
350
|
-
const
|
|
756
|
+
const selectedCursos = useMemo(
|
|
351
757
|
() =>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
758
|
+
learningPathItems
|
|
759
|
+
.filter((item) => item.type === 'course')
|
|
760
|
+
.map((item) => item.itemId),
|
|
761
|
+
[learningPathItems]
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
const trailItems = useMemo<TrailRenderableItem[]>(() => {
|
|
765
|
+
const sorted = [...learningPathItems].sort((a, b) => a.order - b.order);
|
|
766
|
+
|
|
767
|
+
return sorted
|
|
768
|
+
.map((item, index) => {
|
|
769
|
+
if (item.type === 'course') {
|
|
770
|
+
const course = availableCursos.find((c) => c.id === item.itemId);
|
|
771
|
+
if (!course) return null;
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
uid: `course-${course.id}`,
|
|
775
|
+
type: 'course' as const,
|
|
776
|
+
itemId: course.id,
|
|
777
|
+
title: course.nome,
|
|
778
|
+
subtitle: `${course.cargaHoraria}h`,
|
|
779
|
+
order: item.order ?? index,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const exam = availableExams.find((e) => e.id === item.itemId);
|
|
784
|
+
if (!exam) return null;
|
|
785
|
+
|
|
786
|
+
return {
|
|
787
|
+
uid: `exam-${exam.id}`,
|
|
788
|
+
type: 'exam' as const,
|
|
789
|
+
itemId: exam.id,
|
|
790
|
+
title: exam.titulo,
|
|
791
|
+
subtitle: `${exam.limiteTempo}min`,
|
|
792
|
+
order: item.order ?? index,
|
|
793
|
+
};
|
|
794
|
+
})
|
|
795
|
+
.filter(Boolean) as TrailRenderableItem[];
|
|
796
|
+
}, [availableCursos, availableExams, learningPathItems]);
|
|
797
|
+
|
|
798
|
+
const selectableCursos = useMemo(
|
|
799
|
+
() => [...availableCursos].sort((a, b) => a.nome.localeCompare(b.nome)),
|
|
800
|
+
[availableCursos]
|
|
363
801
|
);
|
|
364
802
|
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
803
|
+
const selectableExams = useMemo(
|
|
804
|
+
() => [...availableExams].sort((a, b) => a.titulo.localeCompare(b.titulo)),
|
|
805
|
+
[availableExams]
|
|
368
806
|
);
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
(
|
|
372
|
-
|
|
807
|
+
|
|
808
|
+
const categoryOptions = useMemo<CourseCategoryOption[]>(
|
|
809
|
+
() =>
|
|
810
|
+
(categoryListData?.data ?? [])
|
|
811
|
+
.filter((category) => !!category.slug)
|
|
812
|
+
.map((category) => ({
|
|
813
|
+
value: category.slug,
|
|
814
|
+
label: category.name || category.slug,
|
|
815
|
+
}))
|
|
816
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
817
|
+
[categoryListData]
|
|
373
818
|
);
|
|
374
819
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
if (typeof window === 'undefined') return;
|
|
822
|
+
try {
|
|
823
|
+
const raw = window.localStorage.getItem(API_TRAINING_CACHE_KEY);
|
|
824
|
+
if (!raw) return;
|
|
825
|
+
const parsed = JSON.parse(raw) as ApiTrainingListResponse;
|
|
826
|
+
if (parsed && Array.isArray(parsed.data)) {
|
|
827
|
+
setCachedListData(normalizeTrainingListPayload(parsed));
|
|
828
|
+
}
|
|
829
|
+
} catch {
|
|
830
|
+
setCachedListData(null);
|
|
831
|
+
}
|
|
832
|
+
}, []);
|
|
833
|
+
|
|
834
|
+
useEffect(() => {
|
|
835
|
+
if (typeof window === 'undefined') return;
|
|
836
|
+
if (!listData) return;
|
|
837
|
+
window.localStorage.setItem(
|
|
838
|
+
API_TRAINING_CACHE_KEY,
|
|
839
|
+
JSON.stringify(listData)
|
|
840
|
+
);
|
|
841
|
+
setCachedListData(listData);
|
|
842
|
+
}, [listData]);
|
|
843
|
+
|
|
844
|
+
const effectiveListData = listData ?? cachedListData;
|
|
845
|
+
const initialLoading = isLoadingList && !effectiveListData;
|
|
846
|
+
const cardsRefreshing = isFetchingList && !!effectiveListData;
|
|
847
|
+
|
|
848
|
+
const formacoesToRender = useMemo(() => {
|
|
849
|
+
const formacoes = effectiveListData?.data ?? [];
|
|
850
|
+
|
|
851
|
+
if (!sheetOpen || !editingFormacao) return formacoes;
|
|
852
|
+
|
|
853
|
+
const selectedCourses = availableCursos.filter((course) =>
|
|
854
|
+
selectedCursos.includes(course.id)
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
const selectedCourseNames = selectedCourses.map((course) => course.nome);
|
|
858
|
+
const selectedCourseHours = selectedCourses.reduce(
|
|
859
|
+
(sum, course) => sum + course.cargaHoraria,
|
|
860
|
+
0
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
return formacoes.map((formacao) => {
|
|
864
|
+
if (formacao.id !== editingFormacao.id) return formacao;
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
...formacao,
|
|
868
|
+
nome: watchedFormValues.nome ?? formacao.nome,
|
|
869
|
+
descricao: watchedFormValues.descricao ?? formacao.descricao,
|
|
870
|
+
nivel: watchedFormValues.nivel ?? formacao.nivel,
|
|
871
|
+
status: watchedFormValues.status ?? formacao.status,
|
|
872
|
+
prerequisitos:
|
|
873
|
+
watchedFormValues.prerequisitos ?? formacao.prerequisitos,
|
|
874
|
+
primaryColor: watchedFormValues.primaryColor ?? formacao.primaryColor,
|
|
875
|
+
secondaryColor:
|
|
876
|
+
watchedFormValues.secondaryColor ?? formacao.secondaryColor,
|
|
877
|
+
cursos: selectedCourseNames,
|
|
878
|
+
courseIds: selectedCursos,
|
|
879
|
+
cargaTotal: selectedCourseHours,
|
|
880
|
+
};
|
|
881
|
+
});
|
|
882
|
+
}, [
|
|
883
|
+
availableCursos,
|
|
884
|
+
editingFormacao,
|
|
885
|
+
effectiveListData,
|
|
886
|
+
selectedCursos,
|
|
887
|
+
sheetOpen,
|
|
888
|
+
watchedFormValues,
|
|
889
|
+
]);
|
|
890
|
+
|
|
891
|
+
const totalItems = effectiveListData?.total ?? 0;
|
|
892
|
+
const totalPages = Math.max(effectiveListData?.lastPage ?? 1, 1);
|
|
893
|
+
const safePage = Math.min(currentPage, totalPages);
|
|
894
|
+
|
|
895
|
+
useEffect(() => {
|
|
896
|
+
if (currentPage > totalPages) {
|
|
897
|
+
setCurrentPage(totalPages);
|
|
898
|
+
}
|
|
899
|
+
}, [currentPage, totalPages]);
|
|
900
|
+
|
|
901
|
+
useEffect(() => {
|
|
902
|
+
const timeout = setTimeout(() => {
|
|
903
|
+
setBuscaDebounced(buscaInput.trim());
|
|
904
|
+
}, 350);
|
|
905
|
+
|
|
906
|
+
return () => clearTimeout(timeout);
|
|
907
|
+
}, [buscaInput]);
|
|
908
|
+
|
|
909
|
+
useEffect(() => {
|
|
380
910
|
setCurrentPage(1);
|
|
381
|
-
}
|
|
911
|
+
}, [buscaDebounced, filtroStatusInput, filtroNivelInput]);
|
|
382
912
|
|
|
383
913
|
function clearFilters() {
|
|
384
914
|
setBuscaInput('');
|
|
385
|
-
|
|
915
|
+
setBuscaDebounced('');
|
|
916
|
+
setFiltroStatusInput('todos');
|
|
386
917
|
setFiltroNivelInput('todos');
|
|
387
|
-
setBuscaApplied('');
|
|
388
|
-
setFiltroAreaApplied('todos');
|
|
389
|
-
setFiltroNivelApplied('todos');
|
|
390
918
|
setCurrentPage(1);
|
|
391
919
|
}
|
|
392
920
|
|
|
393
921
|
const hasActiveFilters =
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
922
|
+
buscaInput.trim().length > 0 ||
|
|
923
|
+
filtroStatusInput !== 'todos' ||
|
|
924
|
+
filtroNivelInput !== 'todos';
|
|
925
|
+
|
|
926
|
+
function openDeleteDialog(formacao: Formacao, e: React.MouseEvent) {
|
|
927
|
+
e.stopPropagation();
|
|
928
|
+
setFormacaoToDelete(formacao);
|
|
929
|
+
setDeleteDialogOpen(true);
|
|
930
|
+
}
|
|
397
931
|
|
|
398
932
|
// ── Double-click ──────────────────────────────────────────────────────────
|
|
399
933
|
|
|
@@ -416,85 +950,396 @@ export default function TrainingPage() {
|
|
|
416
950
|
|
|
417
951
|
function openCreateSheet() {
|
|
418
952
|
setEditingFormacao(null);
|
|
419
|
-
|
|
953
|
+
initialLearningPathRef.current = [];
|
|
954
|
+
setLearningPathItems([]);
|
|
420
955
|
form.reset({
|
|
421
956
|
nome: '',
|
|
422
957
|
descricao: '',
|
|
423
|
-
area: '',
|
|
424
|
-
nivel: '',
|
|
958
|
+
area: 'Tecnologia',
|
|
959
|
+
nivel: 'Iniciante',
|
|
425
960
|
prerequisitos: '',
|
|
426
961
|
status: 'rascunho',
|
|
962
|
+
primaryColor: '#1D4ED8',
|
|
963
|
+
secondaryColor: '#111827',
|
|
427
964
|
});
|
|
965
|
+
examForm.reset();
|
|
966
|
+
setSelectedCourseToAdd('');
|
|
967
|
+
setSelectedExamToAdd('');
|
|
968
|
+
void Promise.all([refetchCourses(), refetchExams(), refetchCategories()]);
|
|
428
969
|
setSheetOpen(true);
|
|
429
970
|
}
|
|
430
971
|
|
|
431
|
-
function openEditSheet(formacao: Formacao, e?: React.MouseEvent) {
|
|
972
|
+
async function openEditSheet(formacao: Formacao, e?: React.MouseEvent) {
|
|
432
973
|
e?.stopPropagation();
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
974
|
+
setLoadingEditSheet(true);
|
|
975
|
+
try {
|
|
976
|
+
const [response] = await Promise.all([
|
|
977
|
+
request<Formacao>({
|
|
978
|
+
url: `/lms/training/${formacao.id}`,
|
|
979
|
+
method: 'GET',
|
|
980
|
+
}),
|
|
981
|
+
refetchCourses(),
|
|
982
|
+
refetchExams(),
|
|
983
|
+
refetchCategories(),
|
|
984
|
+
]);
|
|
985
|
+
|
|
986
|
+
const fullFormacao = normalizeTrainingColorPayload(
|
|
987
|
+
(response?.data ?? formacao) as Formacao & TrainingColorPayload
|
|
988
|
+
) as Formacao;
|
|
989
|
+
const normalizedItems: LearningPathItem[] =
|
|
990
|
+
fullFormacao.items && fullFormacao.items.length > 0
|
|
991
|
+
? fullFormacao.items.map((item, index) => ({
|
|
992
|
+
id: item.id,
|
|
993
|
+
type: item.type,
|
|
994
|
+
itemId: item.itemId,
|
|
995
|
+
order: item.order ?? index,
|
|
996
|
+
isRequired: item.isRequired !== false,
|
|
997
|
+
}))
|
|
998
|
+
: (fullFormacao.courseIds ?? []).map((courseId, index) => ({
|
|
999
|
+
type: 'course',
|
|
1000
|
+
itemId: courseId,
|
|
1001
|
+
order: index,
|
|
1002
|
+
isRequired: true,
|
|
1003
|
+
}));
|
|
1004
|
+
|
|
1005
|
+
setEditingFormacao(fullFormacao);
|
|
1006
|
+
initialLearningPathRef.current = [...normalizedItems];
|
|
1007
|
+
setLearningPathItems(normalizedItems);
|
|
1008
|
+
setSelectedCourseToAdd('');
|
|
1009
|
+
setSelectedExamToAdd('');
|
|
1010
|
+
form.reset({
|
|
1011
|
+
nome: fullFormacao.nome,
|
|
1012
|
+
descricao: fullFormacao.descricao,
|
|
1013
|
+
area: normalizeAreaValue(fullFormacao.area),
|
|
1014
|
+
nivel: normalizeLevelValue(fullFormacao.nivel),
|
|
1015
|
+
prerequisitos: fullFormacao.prerequisitos,
|
|
1016
|
+
status: normalizeStatusValue(fullFormacao.status),
|
|
1017
|
+
primaryColor: fullFormacao.primaryColor ?? '#1D4ED8',
|
|
1018
|
+
secondaryColor: fullFormacao.secondaryColor ?? '#111827',
|
|
1019
|
+
});
|
|
1020
|
+
setSheetOpen(true);
|
|
1021
|
+
} finally {
|
|
1022
|
+
setLoadingEditSheet(false);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function normalizeTrailOrder(items: LearningPathItem[]) {
|
|
1027
|
+
return items.map((item, index) => ({ ...item, order: index }));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function addTrailItem(type: 'course' | 'exam', itemId: number) {
|
|
1031
|
+
setLearningPathItems((prev) => {
|
|
1032
|
+
if (prev.some((item) => item.type === type && item.itemId === itemId)) {
|
|
1033
|
+
return prev;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return normalizeTrailOrder([
|
|
1037
|
+
...prev,
|
|
1038
|
+
{
|
|
1039
|
+
type,
|
|
1040
|
+
itemId,
|
|
1041
|
+
order: prev.length,
|
|
1042
|
+
isRequired: true,
|
|
1043
|
+
},
|
|
1044
|
+
]);
|
|
442
1045
|
});
|
|
443
|
-
setSheetOpen(true);
|
|
444
1046
|
}
|
|
445
1047
|
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1048
|
+
function handleCourseSelection(value: string) {
|
|
1049
|
+
setSelectedCourseToAdd(value);
|
|
1050
|
+
const parsed = Number(value);
|
|
1051
|
+
|
|
1052
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1053
|
+
addTrailItem('course', parsed);
|
|
1054
|
+
setSelectedCourseToAdd('');
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function handleExamSelection(value: string) {
|
|
1059
|
+
setSelectedExamToAdd(value);
|
|
1060
|
+
const parsed = Number(value);
|
|
1061
|
+
|
|
1062
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1063
|
+
addTrailItem('exam', parsed);
|
|
1064
|
+
setSelectedExamToAdd('');
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function removeTrailItem(uid: string) {
|
|
1069
|
+
const [type, idText] = uid.split('-');
|
|
1070
|
+
const itemId = Number(idText);
|
|
1071
|
+
|
|
1072
|
+
if (!itemId || (type !== 'course' && type !== 'exam')) return;
|
|
1073
|
+
|
|
1074
|
+
setLearningPathItems((prev) =>
|
|
1075
|
+
normalizeTrailOrder(
|
|
1076
|
+
prev.filter((item) => !(item.type === type && item.itemId === itemId))
|
|
1077
|
+
)
|
|
451
1078
|
);
|
|
452
1079
|
}
|
|
453
1080
|
|
|
1081
|
+
function handleTrailDragEnd(event: DragEndEvent) {
|
|
1082
|
+
const { active, over } = event;
|
|
1083
|
+
if (!over || active.id === over.id) return;
|
|
1084
|
+
|
|
1085
|
+
setLearningPathItems((prev) => {
|
|
1086
|
+
const sorted = [...prev].sort((a, b) => a.order - b.order);
|
|
1087
|
+
const oldIndex = sorted.findIndex(
|
|
1088
|
+
(item) => `${item.type}-${item.itemId}` === String(active.id)
|
|
1089
|
+
);
|
|
1090
|
+
const newIndex = sorted.findIndex(
|
|
1091
|
+
(item) => `${item.type}-${item.itemId}` === String(over.id)
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
if (oldIndex < 0 || newIndex < 0) return prev;
|
|
1095
|
+
|
|
1096
|
+
return normalizeTrailOrder(arrayMove(sorted, oldIndex, newIndex));
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function openCreateCourseSheet() {
|
|
1101
|
+
courseForm.reset(DEFAULT_COURSE_FORM_VALUES);
|
|
1102
|
+
setCourseSheetOpen(true);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function openCreateExamSheet() {
|
|
1106
|
+
examForm.reset({
|
|
1107
|
+
titulo: '',
|
|
1108
|
+
notaMinima: 7,
|
|
1109
|
+
limiteTempo: 60,
|
|
1110
|
+
shuffle: false,
|
|
1111
|
+
status: 'rascunho',
|
|
1112
|
+
});
|
|
1113
|
+
setExamSheetOpen(true);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function ptStatusToApi(value: Formacao['status']) {
|
|
1117
|
+
if (value === 'ativa') return 'active';
|
|
1118
|
+
if (value === 'encerrada') return 'archived';
|
|
1119
|
+
return 'draft';
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function ptLevelToApi(value: string) {
|
|
1123
|
+
const normalized = normalizeText(value);
|
|
1124
|
+
|
|
1125
|
+
if (['iniciante', 'beginner'].includes(normalized)) return 'beginner';
|
|
1126
|
+
if (['intermediario', 'intermediate'].includes(normalized)) {
|
|
1127
|
+
return 'intermediate';
|
|
1128
|
+
}
|
|
1129
|
+
if (['avancado', 'advanced'].includes(normalized)) return 'advanced';
|
|
1130
|
+
|
|
1131
|
+
return 'beginner';
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function courseStatusToApi(value: CourseSheetFormValues['status']) {
|
|
1135
|
+
if (value === 'ativo') return 'published';
|
|
1136
|
+
if (value === 'arquivado') return 'archived';
|
|
1137
|
+
return 'draft';
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function examStatusToApi(value: ExamQuickForm['status']) {
|
|
1141
|
+
if (value === 'publicado') return 'published';
|
|
1142
|
+
if (value === 'encerrado') return 'closed';
|
|
1143
|
+
return 'draft';
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function pathsAreEqual(a: LearningPathItem[], b: LearningPathItem[]) {
|
|
1147
|
+
if (a.length !== b.length) return false;
|
|
1148
|
+
|
|
1149
|
+
return a.every((item, index) => {
|
|
1150
|
+
const other = b[index];
|
|
1151
|
+
if (!other) return false;
|
|
1152
|
+
|
|
1153
|
+
return (
|
|
1154
|
+
item.type === other.type &&
|
|
1155
|
+
item.itemId === other.itemId &&
|
|
1156
|
+
item.order === other.order
|
|
1157
|
+
);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
454
1161
|
async function onSubmit(data: FormacaoForm) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
.filter((c) => selectedCursos.includes(c.nome))
|
|
459
|
-
.reduce((acc, c) => acc + c.cargaHoraria, 0);
|
|
460
|
-
if (editingFormacao) {
|
|
461
|
-
setFormacoes((prev) =>
|
|
462
|
-
prev.map((f) =>
|
|
463
|
-
f.id === editingFormacao.id
|
|
464
|
-
? {
|
|
465
|
-
...f,
|
|
466
|
-
...data,
|
|
467
|
-
status: data.status as Formacao['status'],
|
|
468
|
-
cursos: selectedCursos,
|
|
469
|
-
cargaTotal,
|
|
470
|
-
}
|
|
471
|
-
: f
|
|
472
|
-
)
|
|
1162
|
+
try {
|
|
1163
|
+
const orderedItems = normalizeTrailOrder(
|
|
1164
|
+
[...learningPathItems].sort((a, b) => a.order - b.order)
|
|
473
1165
|
);
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1166
|
+
|
|
1167
|
+
if (orderedItems.length === 0) {
|
|
1168
|
+
toast.error(t('toasts.selectAtLeastOneItem'));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
setSaving(true);
|
|
1173
|
+
|
|
1174
|
+
if (editingFormacao) {
|
|
1175
|
+
const dirty = form.formState.dirtyFields;
|
|
1176
|
+
const itemsChanged = !pathsAreEqual(
|
|
1177
|
+
initialLearningPathRef.current,
|
|
1178
|
+
orderedItems
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
const payload: {
|
|
1182
|
+
title?: string;
|
|
1183
|
+
description?: string;
|
|
1184
|
+
shortDescription?: string;
|
|
1185
|
+
level?: 'beginner' | 'intermediate' | 'advanced';
|
|
1186
|
+
status?: 'draft' | 'active' | 'archived';
|
|
1187
|
+
primaryColor?: string;
|
|
1188
|
+
secondaryColor?: string;
|
|
1189
|
+
items?: Array<{
|
|
1190
|
+
type: 'course' | 'exam';
|
|
1191
|
+
itemId: number;
|
|
1192
|
+
order: number;
|
|
1193
|
+
isRequired: boolean;
|
|
1194
|
+
}>;
|
|
1195
|
+
} = {};
|
|
1196
|
+
|
|
1197
|
+
if (dirty.nome) payload.title = data.nome;
|
|
1198
|
+
if (dirty.descricao) payload.description = data.descricao;
|
|
1199
|
+
if (dirty.prerequisitos) {
|
|
1200
|
+
payload.shortDescription = data.prerequisitos?.trim() || undefined;
|
|
1201
|
+
}
|
|
1202
|
+
if (dirty.nivel) payload.level = ptLevelToApi(data.nivel);
|
|
1203
|
+
if (dirty.status) {
|
|
1204
|
+
payload.status = ptStatusToApi(data.status as Formacao['status']);
|
|
1205
|
+
}
|
|
1206
|
+
if (dirty.primaryColor) payload.primaryColor = data.primaryColor;
|
|
1207
|
+
if (dirty.secondaryColor) payload.secondaryColor = data.secondaryColor;
|
|
1208
|
+
if (itemsChanged) {
|
|
1209
|
+
payload.items = orderedItems.map((item, index) => ({
|
|
1210
|
+
type: item.type,
|
|
1211
|
+
itemId: item.itemId,
|
|
1212
|
+
order: index,
|
|
1213
|
+
isRequired: item.isRequired !== false,
|
|
1214
|
+
}));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (Object.keys(payload).length === 0) {
|
|
1218
|
+
setSheetOpen(false);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
await request({
|
|
1223
|
+
url: `/lms/training/${editingFormacao.id}`,
|
|
1224
|
+
method: 'PATCH',
|
|
1225
|
+
data: payload,
|
|
1226
|
+
});
|
|
1227
|
+
toast.success(t('toasts.formacaoUpdated'));
|
|
1228
|
+
} else {
|
|
1229
|
+
const payload = {
|
|
1230
|
+
title: data.nome,
|
|
1231
|
+
description: data.descricao,
|
|
1232
|
+
shortDescription: data.prerequisitos?.trim() || undefined,
|
|
1233
|
+
level: ptLevelToApi(data.nivel),
|
|
1234
|
+
status: ptStatusToApi(data.status as Formacao['status']),
|
|
1235
|
+
primaryColor: data.primaryColor,
|
|
1236
|
+
secondaryColor: data.secondaryColor,
|
|
1237
|
+
items: orderedItems.map((item, index) => ({
|
|
1238
|
+
type: item.type,
|
|
1239
|
+
itemId: item.itemId,
|
|
1240
|
+
order: index,
|
|
1241
|
+
isRequired: item.isRequired !== false,
|
|
1242
|
+
})),
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
await request({
|
|
1246
|
+
url: '/lms/training',
|
|
1247
|
+
method: 'POST',
|
|
1248
|
+
data: payload,
|
|
1249
|
+
});
|
|
1250
|
+
toast.success(t('toasts.formacaoCriada'));
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
await Promise.all([refetchTraining(), refetchStats()]);
|
|
1254
|
+
setSheetOpen(false);
|
|
1255
|
+
setLearningPathItems([]);
|
|
1256
|
+
initialLearningPathRef.current = [];
|
|
1257
|
+
} finally {
|
|
1258
|
+
setSaving(false);
|
|
488
1259
|
}
|
|
489
|
-
setSaving(false);
|
|
490
|
-
setSheetOpen(false);
|
|
491
|
-
setSelectedCursos([]);
|
|
492
1260
|
}
|
|
493
1261
|
|
|
494
|
-
function
|
|
1262
|
+
async function onSubmitCourse(data: CourseSheetFormValues) {
|
|
1263
|
+
try {
|
|
1264
|
+
setCreatingCourse(true);
|
|
1265
|
+
|
|
1266
|
+
const slug = slugifyText(data.nomeInterno || data.tituloComercial);
|
|
1267
|
+
const categorySlugs = data.categorias.map(slugifyText).filter(Boolean);
|
|
1268
|
+
|
|
1269
|
+
const response = await request<{
|
|
1270
|
+
id?: number;
|
|
1271
|
+
}>({
|
|
1272
|
+
url: '/lms/courses',
|
|
1273
|
+
method: 'POST',
|
|
1274
|
+
data: {
|
|
1275
|
+
code: buildCourseCodeFromTitle(
|
|
1276
|
+
data.tituloComercial || data.nomeInterno
|
|
1277
|
+
),
|
|
1278
|
+
slug,
|
|
1279
|
+
title: data.tituloComercial,
|
|
1280
|
+
description: data.descricao,
|
|
1281
|
+
level: ptLevelToApi(data.nivel),
|
|
1282
|
+
status: courseStatusToApi(data.status),
|
|
1283
|
+
categorySlugs,
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
const createdCourseId = Number(response?.data?.id);
|
|
1288
|
+
|
|
1289
|
+
await refetchCourses();
|
|
1290
|
+
if (Number.isFinite(createdCourseId) && createdCourseId > 0) {
|
|
1291
|
+
addTrailItem('course', createdCourseId);
|
|
1292
|
+
setSelectedCourseToAdd('');
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
setCourseSheetOpen(false);
|
|
1296
|
+
toast.success(t('toasts.courseCreated'));
|
|
1297
|
+
} finally {
|
|
1298
|
+
setCreatingCourse(false);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async function onSubmitExam(data: ExamQuickForm) {
|
|
1303
|
+
try {
|
|
1304
|
+
setCreatingExam(true);
|
|
1305
|
+
|
|
1306
|
+
const response = await request<{ id?: number }>({
|
|
1307
|
+
url: '/lms/exams',
|
|
1308
|
+
method: 'POST',
|
|
1309
|
+
data: {
|
|
1310
|
+
title: data.titulo,
|
|
1311
|
+
minScore: data.notaMinima,
|
|
1312
|
+
timeLimit: data.limiteTempo,
|
|
1313
|
+
shuffle: data.shuffle,
|
|
1314
|
+
status: examStatusToApi(data.status),
|
|
1315
|
+
},
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
const createdExamId = Number(response?.data?.id);
|
|
1319
|
+
|
|
1320
|
+
await refetchExams();
|
|
1321
|
+
if (Number.isFinite(createdExamId) && createdExamId > 0) {
|
|
1322
|
+
addTrailItem('exam', createdExamId);
|
|
1323
|
+
setSelectedExamToAdd('');
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
setExamSheetOpen(false);
|
|
1327
|
+
toast.success(t('toasts.examCreated'));
|
|
1328
|
+
} finally {
|
|
1329
|
+
setCreatingExam(false);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
async function confirmDelete() {
|
|
495
1334
|
if (!formacaoToDelete) return;
|
|
496
|
-
|
|
1335
|
+
|
|
1336
|
+
await request({
|
|
1337
|
+
url: `/lms/training/${formacaoToDelete.id}`,
|
|
1338
|
+
method: 'DELETE',
|
|
1339
|
+
});
|
|
1340
|
+
|
|
497
1341
|
toast.success(t('toasts.formacaoRemovida'));
|
|
1342
|
+
await Promise.all([refetchTraining(), refetchStats()]);
|
|
498
1343
|
setFormacaoToDelete(null);
|
|
499
1344
|
setDeleteDialogOpen(false);
|
|
500
1345
|
}
|
|
@@ -503,41 +1348,60 @@ export default function TrainingPage() {
|
|
|
503
1348
|
|
|
504
1349
|
const kpis = [
|
|
505
1350
|
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1351
|
+
key: 'total-training',
|
|
1352
|
+
title: t('kpis.totalTraining.label'),
|
|
1353
|
+
value: statsData?.totalTraining ?? 0,
|
|
1354
|
+
description: t('kpis.totalTraining.sub'),
|
|
509
1355
|
icon: GraduationCap,
|
|
510
|
-
|
|
511
|
-
|
|
1356
|
+
layout: 'compact' as const,
|
|
1357
|
+
accentClassName: 'from-orange-500/25 via-amber-500/15 to-transparent',
|
|
1358
|
+
iconContainerClassName: 'bg-orange-100 text-orange-700',
|
|
512
1359
|
},
|
|
513
1360
|
{
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1361
|
+
key: 'active-training',
|
|
1362
|
+
title: t('kpis.activeTraining.label'),
|
|
1363
|
+
value: statsData?.activeTraining ?? 0,
|
|
1364
|
+
description: t('kpis.activeTraining.sub'),
|
|
517
1365
|
icon: Target,
|
|
518
|
-
|
|
519
|
-
|
|
1366
|
+
layout: 'compact' as const,
|
|
1367
|
+
accentClassName: 'from-sky-500/25 via-blue-500/15 to-transparent',
|
|
1368
|
+
iconContainerClassName: 'bg-sky-100 text-sky-700',
|
|
520
1369
|
},
|
|
521
1370
|
{
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
sub: t('kpis.enrolledStudents.sub'),
|
|
1371
|
+
key: 'enrolled-students',
|
|
1372
|
+
title: t('kpis.enrolledStudents.label'),
|
|
1373
|
+
value: (statsData?.enrolledStudents ?? 0).toLocaleString('pt-BR'),
|
|
1374
|
+
description: t('kpis.enrolledStudents.sub'),
|
|
527
1375
|
icon: Users,
|
|
528
|
-
|
|
529
|
-
|
|
1376
|
+
layout: 'compact' as const,
|
|
1377
|
+
accentClassName: 'from-emerald-500/25 via-green-500/15 to-transparent',
|
|
1378
|
+
iconContainerClassName: 'bg-emerald-100 text-emerald-700',
|
|
530
1379
|
},
|
|
531
1380
|
{
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1381
|
+
key: 'covered-courses',
|
|
1382
|
+
title: t('kpis.coveredCourses.label'),
|
|
1383
|
+
value: statsData?.coveredCourses ?? 0,
|
|
1384
|
+
description: t('kpis.coveredCourses.sub'),
|
|
535
1385
|
icon: Layers,
|
|
536
|
-
|
|
537
|
-
|
|
1386
|
+
layout: 'compact' as const,
|
|
1387
|
+
accentClassName: 'from-pink-500/25 via-rose-500/15 to-transparent',
|
|
1388
|
+
iconContainerClassName: 'bg-pink-100 text-pink-700',
|
|
538
1389
|
},
|
|
539
1390
|
];
|
|
540
1391
|
|
|
1392
|
+
const { locales } = useApp();
|
|
1393
|
+
|
|
1394
|
+
const handleNewTraining = (): void => {
|
|
1395
|
+
const nextLocaleData: Record<string, { name: string }> = {};
|
|
1396
|
+
locales.forEach((locale: Locale) => {
|
|
1397
|
+
nextLocaleData[locale.code] = {
|
|
1398
|
+
name: '',
|
|
1399
|
+
};
|
|
1400
|
+
});
|
|
1401
|
+
void nextLocaleData;
|
|
1402
|
+
openCreateSheet();
|
|
1403
|
+
};
|
|
1404
|
+
|
|
541
1405
|
// ── Render ────────────────────────────────────────────────────────────────
|
|
542
1406
|
|
|
543
1407
|
return (
|
|
@@ -554,298 +1418,438 @@ export default function TrainingPage() {
|
|
|
554
1418
|
label: t('title'),
|
|
555
1419
|
},
|
|
556
1420
|
]}
|
|
557
|
-
actions={
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1421
|
+
actions={[
|
|
1422
|
+
{
|
|
1423
|
+
label: t('actions.createTraining'),
|
|
1424
|
+
onClick: () => handleNewTraining(),
|
|
1425
|
+
variant: 'default',
|
|
1426
|
+
},
|
|
1427
|
+
]}
|
|
563
1428
|
/>
|
|
564
1429
|
|
|
565
1430
|
{/* KPIs */}
|
|
566
|
-
<div className="mb-
|
|
567
|
-
{
|
|
568
|
-
|
|
1431
|
+
<div className="mb-1">
|
|
1432
|
+
{initialLoading && !statsData ? (
|
|
1433
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
1434
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
569
1435
|
<Card key={i}>
|
|
570
1436
|
<CardContent className="p-4">
|
|
571
1437
|
<Skeleton className="mb-2 h-8 w-16" />
|
|
572
1438
|
<Skeleton className="h-4 w-28" />
|
|
573
1439
|
</CardContent>
|
|
574
1440
|
</Card>
|
|
575
|
-
))
|
|
576
|
-
: kpis.map((kpi, i) => (
|
|
577
|
-
<motion.div
|
|
578
|
-
key={kpi.label}
|
|
579
|
-
initial={{ opacity: 0, y: 12 }}
|
|
580
|
-
animate={{ opacity: 1, y: 0 }}
|
|
581
|
-
transition={{ delay: i * 0.07 }}
|
|
582
|
-
>
|
|
583
|
-
<Card className="overflow-hidden">
|
|
584
|
-
<CardContent className="flex items-start justify-between p-5">
|
|
585
|
-
<div>
|
|
586
|
-
<p className="text-sm text-muted-foreground">
|
|
587
|
-
{kpi.label}
|
|
588
|
-
</p>
|
|
589
|
-
<p className="mt-1 text-3xl font-bold tracking-tight">
|
|
590
|
-
{kpi.valor}
|
|
591
|
-
</p>
|
|
592
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
593
|
-
{kpi.sub}
|
|
594
|
-
</p>
|
|
595
|
-
</div>
|
|
596
|
-
<div
|
|
597
|
-
className={`flex size-10 shrink-0 items-center justify-center rounded-lg ${kpi.iconBg}`}
|
|
598
|
-
>
|
|
599
|
-
<kpi.icon className={`size-5 ${kpi.iconColor}`} />
|
|
600
|
-
</div>
|
|
601
|
-
</CardContent>
|
|
602
|
-
</Card>
|
|
603
|
-
</motion.div>
|
|
604
1441
|
))}
|
|
1442
|
+
</div>
|
|
1443
|
+
) : (
|
|
1444
|
+
<KpiCardsGrid items={kpis} />
|
|
1445
|
+
)}
|
|
605
1446
|
</div>
|
|
606
1447
|
|
|
607
1448
|
{/* Search bar */}
|
|
608
|
-
<
|
|
609
|
-
<
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1449
|
+
<div className="mb-1 space-y-3">
|
|
1450
|
+
<SearchBar
|
|
1451
|
+
searchQuery={buscaInput}
|
|
1452
|
+
onSearchChange={setBuscaInput}
|
|
1453
|
+
onSearch={() => {
|
|
1454
|
+
setBuscaDebounced(buscaInput.trim());
|
|
1455
|
+
setCurrentPage(1);
|
|
1456
|
+
}}
|
|
1457
|
+
placeholder={t('filters.searchPlaceholder')}
|
|
1458
|
+
controls={[
|
|
1459
|
+
{
|
|
1460
|
+
id: 'status',
|
|
1461
|
+
type: 'select',
|
|
1462
|
+
value: filtroStatusInput,
|
|
1463
|
+
onChange: setFiltroStatusInput,
|
|
1464
|
+
placeholder: t('filters.allStatuses'),
|
|
1465
|
+
options: [
|
|
1466
|
+
{ value: 'todos', label: t('filters.allStatuses') },
|
|
1467
|
+
{ value: 'ativa', label: t('status.active') },
|
|
1468
|
+
{ value: 'rascunho', label: t('status.draft') },
|
|
1469
|
+
{ value: 'encerrada', label: t('status.closed') },
|
|
1470
|
+
],
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
id: 'level',
|
|
1474
|
+
type: 'select',
|
|
1475
|
+
value: filtroNivelInput,
|
|
1476
|
+
onChange: setFiltroNivelInput,
|
|
1477
|
+
placeholder: t('filters.allLevels'),
|
|
1478
|
+
options: [
|
|
1479
|
+
{ value: 'todos', label: t('filters.allLevels') },
|
|
1480
|
+
{ value: 'Iniciante', label: t('levels.beginner') },
|
|
1481
|
+
{ value: 'Intermediario', label: t('levels.intermediate') },
|
|
1482
|
+
{ value: 'Avancado', label: t('levels.advanced') },
|
|
1483
|
+
],
|
|
1484
|
+
},
|
|
1485
|
+
]}
|
|
1486
|
+
afterSearchButton={
|
|
1487
|
+
<ViewModeToggle
|
|
1488
|
+
viewMode={viewMode}
|
|
1489
|
+
onViewModeChange={setViewMode}
|
|
1490
|
+
listLabel={t('viewMode.list')}
|
|
1491
|
+
cardsLabel={t('viewMode.cards')}
|
|
617
1492
|
/>
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
</SelectItem>
|
|
629
|
-
<SelectItem value="Design">{t('areas.design')}</SelectItem>
|
|
630
|
-
<SelectItem value="Gestao">{t('areas.management')}</SelectItem>
|
|
631
|
-
<SelectItem value="Marketing">
|
|
632
|
-
{t('areas.marketing')}
|
|
633
|
-
</SelectItem>
|
|
634
|
-
<SelectItem value="Financas">{t('areas.finance')}</SelectItem>
|
|
635
|
-
</SelectContent>
|
|
636
|
-
</Select>
|
|
637
|
-
<Select
|
|
638
|
-
value={filtroNivelInput}
|
|
639
|
-
onValueChange={setFiltroNivelInput}
|
|
640
|
-
>
|
|
641
|
-
<SelectTrigger className="h-9 w-[130px] text-sm">
|
|
642
|
-
<SelectValue placeholder={t('filters.allLevels')} />
|
|
643
|
-
</SelectTrigger>
|
|
644
|
-
<SelectContent>
|
|
645
|
-
<SelectItem value="todos">{t('filters.allLevels')}</SelectItem>
|
|
646
|
-
<SelectItem value="Iniciante">
|
|
647
|
-
{t('levels.beginner')}
|
|
648
|
-
</SelectItem>
|
|
649
|
-
<SelectItem value="Intermediario">
|
|
650
|
-
{t('levels.intermediate')}
|
|
651
|
-
</SelectItem>
|
|
652
|
-
<SelectItem value="Avancado">{t('levels.advanced')}</SelectItem>
|
|
653
|
-
</SelectContent>
|
|
654
|
-
</Select>
|
|
1493
|
+
}
|
|
1494
|
+
/>
|
|
1495
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1496
|
+
<p className="text-sm text-muted-foreground">
|
|
1497
|
+
{totalItems}{' '}
|
|
1498
|
+
{totalItems !== 1
|
|
1499
|
+
? t('pagination.formacoes')
|
|
1500
|
+
: t('pagination.formacao')}
|
|
1501
|
+
</p>
|
|
1502
|
+
<div className="flex items-center gap-2">
|
|
655
1503
|
{hasActiveFilters && (
|
|
656
1504
|
<Button
|
|
657
1505
|
type="button"
|
|
658
1506
|
variant="ghost"
|
|
659
1507
|
size="sm"
|
|
660
1508
|
onClick={clearFilters}
|
|
661
|
-
className="h-9 text-muted-foreground"
|
|
662
1509
|
>
|
|
663
|
-
|
|
1510
|
+
{t('filters.clear')}
|
|
664
1511
|
</Button>
|
|
665
1512
|
)}
|
|
666
|
-
|
|
667
|
-
<
|
|
668
|
-
|
|
1513
|
+
{cardsRefreshing && (
|
|
1514
|
+
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
|
1515
|
+
)}
|
|
669
1516
|
</div>
|
|
670
1517
|
</div>
|
|
671
|
-
</
|
|
672
|
-
|
|
673
|
-
{/*
|
|
674
|
-
{
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
<
|
|
679
|
-
<
|
|
680
|
-
<
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1518
|
+
</div>
|
|
1519
|
+
|
|
1520
|
+
{/* Training list */}
|
|
1521
|
+
{initialLoading ? (
|
|
1522
|
+
viewMode === 'cards' ? (
|
|
1523
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
1524
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
1525
|
+
<Card key={i} className="overflow-hidden">
|
|
1526
|
+
<CardContent className="p-5">
|
|
1527
|
+
<div className="flex min-h-52.5 flex-col justify-between">
|
|
1528
|
+
<div>
|
|
1529
|
+
<Skeleton className="mb-3 h-5 w-20 rounded-full" />
|
|
1530
|
+
<Skeleton className="mb-1.5 h-5 w-3/4" />
|
|
1531
|
+
<Skeleton className="mb-4 h-4 w-full" />
|
|
1532
|
+
</div>
|
|
1533
|
+
<div className="mt-auto flex gap-2">
|
|
1534
|
+
<Skeleton className="h-6 w-16 rounded-full" />
|
|
1535
|
+
<Skeleton className="h-6 w-20 rounded-full" />
|
|
1536
|
+
</div>
|
|
1537
|
+
</div>
|
|
1538
|
+
</CardContent>
|
|
1539
|
+
</Card>
|
|
1540
|
+
))}
|
|
1541
|
+
</div>
|
|
1542
|
+
) : (
|
|
1543
|
+
<div className="overflow-hidden rounded-xl border border-border/70">
|
|
1544
|
+
<Table>
|
|
1545
|
+
<TableHeader>
|
|
1546
|
+
<TableRow>
|
|
1547
|
+
<TableHead>{t('form.fields.nome.label')}</TableHead>
|
|
1548
|
+
<TableHead>{t('form.fields.level.label')}</TableHead>
|
|
1549
|
+
<TableHead>{t('form.fields.status.label')}</TableHead>
|
|
1550
|
+
<TableHead>{t('cards.coursesLabel')}</TableHead>
|
|
1551
|
+
<TableHead>{t('cards.hoursLabel')}</TableHead>
|
|
1552
|
+
<TableHead className="text-right">
|
|
1553
|
+
{t('cards.studentsLabel')}
|
|
1554
|
+
</TableHead>
|
|
1555
|
+
<TableHead className="w-12" />
|
|
1556
|
+
</TableRow>
|
|
1557
|
+
</TableHeader>
|
|
1558
|
+
<TableBody>
|
|
1559
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
1560
|
+
<TableRow key={i}>
|
|
1561
|
+
<TableCell>
|
|
1562
|
+
<div className="space-y-1.5">
|
|
1563
|
+
<Skeleton className="h-4 w-44" />
|
|
1564
|
+
<Skeleton className="h-3 w-56" />
|
|
1565
|
+
</div>
|
|
1566
|
+
</TableCell>
|
|
1567
|
+
<TableCell>
|
|
1568
|
+
<Skeleton className="h-5 w-20 rounded-full" />
|
|
1569
|
+
</TableCell>
|
|
1570
|
+
<TableCell>
|
|
1571
|
+
<Skeleton className="h-5 w-20 rounded-full" />
|
|
1572
|
+
</TableCell>
|
|
1573
|
+
<TableCell>
|
|
1574
|
+
<Skeleton className="h-4 w-12" />
|
|
1575
|
+
</TableCell>
|
|
1576
|
+
<TableCell>
|
|
1577
|
+
<Skeleton className="h-4 w-10" />
|
|
1578
|
+
</TableCell>
|
|
1579
|
+
<TableCell className="text-right">
|
|
1580
|
+
<Skeleton className="ml-auto h-4 w-12" />
|
|
1581
|
+
</TableCell>
|
|
1582
|
+
<TableCell>
|
|
1583
|
+
<Skeleton className="ml-auto size-8 rounded-md" />
|
|
1584
|
+
</TableCell>
|
|
1585
|
+
</TableRow>
|
|
1586
|
+
))}
|
|
1587
|
+
</TableBody>
|
|
1588
|
+
</Table>
|
|
1589
|
+
</div>
|
|
1590
|
+
)
|
|
1591
|
+
) : totalItems === 0 ? (
|
|
693
1592
|
<EmptyState
|
|
694
|
-
icon={<GraduationCap className="
|
|
1593
|
+
icon={<GraduationCap className="size-12 text-muted-foreground/40" />}
|
|
695
1594
|
title={t('empty.title')}
|
|
696
1595
|
description={t('empty.description')}
|
|
697
1596
|
actionLabel={t('empty.action')}
|
|
1597
|
+
actionIcon={<Plus className="mr-2 size-4" />}
|
|
698
1598
|
onAction={openCreateSheet}
|
|
699
|
-
|
|
1599
|
+
className="py-20"
|
|
700
1600
|
/>
|
|
701
1601
|
) : (
|
|
702
|
-
<
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
className="group relative flex min-h-60 max-h-[270px] cursor-pointer flex-col overflow-hidden transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
|
|
721
|
-
onClick={() => handleCardClick(formacao)}
|
|
722
|
-
title={t('cards.tooltip')}
|
|
723
|
-
>
|
|
724
|
-
<CardContent className="p-5 flex flex-col h-full">
|
|
725
|
-
{/* Top */}
|
|
726
|
-
<div className="mb-3 flex items-center justify-between gap-2">
|
|
727
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
728
|
-
<span
|
|
729
|
-
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${areaColor}`}
|
|
730
|
-
>
|
|
731
|
-
{formacao.area}
|
|
732
|
-
</span>
|
|
733
|
-
<Badge variant={statusCfg.variant} className="text-xs">
|
|
734
|
-
{statusCfg.label}
|
|
735
|
-
</Badge>
|
|
736
|
-
</div>
|
|
737
|
-
<DropdownMenu>
|
|
738
|
-
<DropdownMenuTrigger asChild>
|
|
739
|
-
<Button
|
|
740
|
-
variant="ghost"
|
|
741
|
-
size="icon"
|
|
742
|
-
className="size-8 shrink-0 -mr-2 -mt-1"
|
|
743
|
-
onClick={(e) => e.stopPropagation()}
|
|
744
|
-
aria-label={t('cards.actions.label')}
|
|
745
|
-
>
|
|
746
|
-
<MoreHorizontal className="size-4" />
|
|
747
|
-
</Button>
|
|
748
|
-
</DropdownMenuTrigger>
|
|
749
|
-
<DropdownMenuContent align="end" className="w-48">
|
|
750
|
-
<DropdownMenuItem
|
|
751
|
-
onClick={(e) => {
|
|
752
|
-
e.stopPropagation();
|
|
753
|
-
toast.info(t('toasts.openingDetails'));
|
|
754
|
-
}}
|
|
755
|
-
>
|
|
756
|
-
<Eye className="mr-2 size-4" />{' '}
|
|
757
|
-
{t('cards.actions.viewDetails')}
|
|
758
|
-
</DropdownMenuItem>
|
|
759
|
-
<DropdownMenuItem
|
|
760
|
-
onClick={(e) => openEditSheet(formacao, e)}
|
|
761
|
-
>
|
|
762
|
-
<Pencil className="mr-2 size-4" />{' '}
|
|
763
|
-
{t('cards.actions.edit')}
|
|
764
|
-
</DropdownMenuItem>
|
|
765
|
-
<DropdownMenuSeparator />
|
|
766
|
-
<DropdownMenuItem
|
|
767
|
-
className="text-destructive focus:text-destructive"
|
|
768
|
-
onClick={(e) => {
|
|
769
|
-
e.stopPropagation();
|
|
770
|
-
setFormacaoToDelete(formacao);
|
|
771
|
-
setDeleteDialogOpen(true);
|
|
772
|
-
}}
|
|
773
|
-
>
|
|
774
|
-
<Trash2 className="mr-2 size-4" />{' '}
|
|
775
|
-
{t('cards.actions.delete')}
|
|
776
|
-
</DropdownMenuItem>
|
|
777
|
-
</DropdownMenuContent>
|
|
778
|
-
</DropdownMenu>
|
|
779
|
-
</div>
|
|
1602
|
+
<div className="relative">
|
|
1603
|
+
{cardsRefreshing && (
|
|
1604
|
+
<div className="absolute inset-0 z-10 rounded-2xl bg-background/55 backdrop-blur-[1px]" />
|
|
1605
|
+
)}
|
|
1606
|
+
{viewMode === 'cards' ? (
|
|
1607
|
+
<motion.div
|
|
1608
|
+
className={`${'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'} ${cardsRefreshing ? 'pointer-events-none' : ''}`}
|
|
1609
|
+
variants={stagger}
|
|
1610
|
+
initial="hidden"
|
|
1611
|
+
animate="show"
|
|
1612
|
+
>
|
|
1613
|
+
{formacoesToRender.map((formacao) => {
|
|
1614
|
+
const statusCfg = STATUS_MAP[formacao.status] ?? {
|
|
1615
|
+
label: formacao.status,
|
|
1616
|
+
variant: 'default' as const,
|
|
1617
|
+
};
|
|
1618
|
+
const cardTopColor =
|
|
1619
|
+
normalizeHexColor(formacao.primaryColor) ?? '#1D4ED8';
|
|
780
1620
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
{
|
|
787
|
-
|
|
1621
|
+
return (
|
|
1622
|
+
<motion.div key={formacao.id} variants={fadeUp}>
|
|
1623
|
+
<Card
|
|
1624
|
+
className="group relative flex min-h-60 max-h-67.5 cursor-pointer flex-col overflow-hidden border-border/70 py-0 shadow-sm transition-all duration-200 hover:border-border hover:shadow-md"
|
|
1625
|
+
onClick={() => handleCardClick(formacao)}
|
|
1626
|
+
title={t('cards.tooltip')}
|
|
1627
|
+
>
|
|
1628
|
+
<div
|
|
1629
|
+
className="absolute inset-x-0 top-0 h-1"
|
|
1630
|
+
style={{
|
|
1631
|
+
backgroundColor: cardTopColor,
|
|
1632
|
+
}}
|
|
1633
|
+
/>
|
|
1634
|
+
<CardContent className="flex h-full flex-col p-4 pt-5">
|
|
1635
|
+
<div className="mb-3 flex items-center justify-between gap-2">
|
|
1636
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
1637
|
+
<Badge variant="outline" className="text-xs">
|
|
1638
|
+
{normalizeLevelValue(formacao.nivel)}
|
|
1639
|
+
</Badge>
|
|
1640
|
+
<Badge
|
|
1641
|
+
variant={statusCfg.variant}
|
|
1642
|
+
className="text-xs"
|
|
1643
|
+
>
|
|
1644
|
+
{statusCfg.label}
|
|
1645
|
+
</Badge>
|
|
1646
|
+
</div>
|
|
1647
|
+
<DropdownMenu>
|
|
1648
|
+
<DropdownMenuTrigger asChild>
|
|
1649
|
+
<Button
|
|
1650
|
+
variant="ghost"
|
|
1651
|
+
size="icon"
|
|
1652
|
+
className="size-8 shrink-0 -mr-2 -mt-1"
|
|
1653
|
+
onClick={(e) => e.stopPropagation()}
|
|
1654
|
+
aria-label={t('cards.actions.label')}
|
|
1655
|
+
>
|
|
1656
|
+
<MoreHorizontal className="size-4" />
|
|
1657
|
+
</Button>
|
|
1658
|
+
</DropdownMenuTrigger>
|
|
1659
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
1660
|
+
<DropdownMenuItem
|
|
1661
|
+
onClick={(e) => openEditSheet(formacao, e)}
|
|
1662
|
+
>
|
|
1663
|
+
<Pencil className="mr-2 size-4" />{' '}
|
|
1664
|
+
{t('cards.actions.edit')}
|
|
1665
|
+
</DropdownMenuItem>
|
|
1666
|
+
<DropdownMenuSeparator />
|
|
1667
|
+
<DropdownMenuItem
|
|
1668
|
+
className="text-destructive focus:text-destructive"
|
|
1669
|
+
onClick={(e) => openDeleteDialog(formacao, e)}
|
|
1670
|
+
>
|
|
1671
|
+
<Trash2 className="mr-2 size-4" />{' '}
|
|
1672
|
+
{t('cards.actions.delete')}
|
|
1673
|
+
</DropdownMenuItem>
|
|
1674
|
+
</DropdownMenuContent>
|
|
1675
|
+
</DropdownMenu>
|
|
1676
|
+
</div>
|
|
788
1677
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
<
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
>
|
|
797
|
-
{
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1678
|
+
<h3 className="mb-0.5 font-semibold leading-tight">
|
|
1679
|
+
{formacao.nome}
|
|
1680
|
+
</h3>
|
|
1681
|
+
<p className="mb-4 line-clamp-2 text-sm text-muted-foreground leading-relaxed">
|
|
1682
|
+
{formacao.descricao}
|
|
1683
|
+
</p>
|
|
1684
|
+
|
|
1685
|
+
<p className="mb-3 line-clamp-1 text-xs text-muted-foreground">
|
|
1686
|
+
{t('cards.prerequisiteLabel')}{' '}
|
|
1687
|
+
{formacao.prerequisitos?.trim()
|
|
1688
|
+
? formacao.prerequisitos
|
|
1689
|
+
: t('cards.noPrerequisite')}
|
|
1690
|
+
</p>
|
|
1691
|
+
|
|
1692
|
+
<div className="mb-4 flex flex-wrap gap-1">
|
|
1693
|
+
{formacao.cursos.slice(0, 3).map((c) => (
|
|
1694
|
+
<Badge
|
|
1695
|
+
key={c}
|
|
1696
|
+
variant="outline"
|
|
1697
|
+
className="text-xs px-1.5 py-0"
|
|
1698
|
+
>
|
|
1699
|
+
{c}
|
|
1700
|
+
</Badge>
|
|
1701
|
+
))}
|
|
1702
|
+
{formacao.cursos.length > 3 && (
|
|
1703
|
+
<Badge
|
|
1704
|
+
variant="outline"
|
|
1705
|
+
className="text-xs px-1.5 py-0"
|
|
1706
|
+
>
|
|
1707
|
+
+{formacao.cursos.length - 3}
|
|
1708
|
+
</Badge>
|
|
1709
|
+
)}
|
|
1710
|
+
</div>
|
|
809
1711
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1712
|
+
<Separator className="mb-3" />
|
|
1713
|
+
|
|
1714
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
1715
|
+
<div className="flex items-center gap-3">
|
|
1716
|
+
<span className="flex items-center gap-1">
|
|
1717
|
+
<Layers className="size-3.5" />
|
|
1718
|
+
{formacao.cursos.length} {t('cards.coursesLabel')}
|
|
1719
|
+
</span>
|
|
1720
|
+
<span className="flex items-center gap-1">
|
|
1721
|
+
<Clock className="size-3.5" />
|
|
1722
|
+
{formacao.cargaTotal}
|
|
1723
|
+
{t('cards.hoursLabel')}
|
|
1724
|
+
</span>
|
|
1725
|
+
</div>
|
|
1726
|
+
<span className="flex items-center gap-1">
|
|
1727
|
+
<Users className="size-3.5" />
|
|
1728
|
+
{formacao.alunos.toLocaleString('pt-BR')}{' '}
|
|
1729
|
+
{t('cards.studentsLabel')}
|
|
1730
|
+
</span>
|
|
1731
|
+
</div>
|
|
1732
|
+
</CardContent>
|
|
1733
|
+
</Card>
|
|
1734
|
+
</motion.div>
|
|
1735
|
+
);
|
|
1736
|
+
})}
|
|
1737
|
+
</motion.div>
|
|
1738
|
+
) : (
|
|
1739
|
+
<div
|
|
1740
|
+
className={`overflow-hidden rounded-xl border border-border/70 ${cardsRefreshing ? 'pointer-events-none' : ''}`}
|
|
1741
|
+
>
|
|
1742
|
+
<Table>
|
|
1743
|
+
<TableHeader>
|
|
1744
|
+
<TableRow>
|
|
1745
|
+
<TableHead>{t('form.fields.nome.label')}</TableHead>
|
|
1746
|
+
<TableHead>{t('form.fields.level.label')}</TableHead>
|
|
1747
|
+
<TableHead>{t('form.fields.status.label')}</TableHead>
|
|
1748
|
+
<TableHead>{t('cards.coursesLabel')}</TableHead>
|
|
1749
|
+
<TableHead>{t('cards.hoursLabel')}</TableHead>
|
|
1750
|
+
<TableHead className="text-right">
|
|
1751
|
+
{t('cards.studentsLabel')}
|
|
1752
|
+
</TableHead>
|
|
1753
|
+
<TableHead className="w-12" />
|
|
1754
|
+
</TableRow>
|
|
1755
|
+
</TableHeader>
|
|
1756
|
+
<TableBody>
|
|
1757
|
+
{formacoesToRender.map((formacao) => {
|
|
1758
|
+
const statusCfg = STATUS_MAP[formacao.status] ?? {
|
|
1759
|
+
label: formacao.status,
|
|
1760
|
+
variant: 'default' as const,
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
return (
|
|
1764
|
+
<TableRow
|
|
1765
|
+
key={formacao.id}
|
|
1766
|
+
className="cursor-pointer"
|
|
1767
|
+
onClick={() => handleCardClick(formacao)}
|
|
1768
|
+
title={t('cards.tooltip')}
|
|
1769
|
+
>
|
|
1770
|
+
<TableCell>
|
|
1771
|
+
<div className="min-w-0">
|
|
1772
|
+
<p className="truncate font-semibold text-foreground">
|
|
1773
|
+
{formacao.nome}
|
|
1774
|
+
</p>
|
|
1775
|
+
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
|
|
1776
|
+
{formacao.prerequisitos?.trim()
|
|
1777
|
+
? formacao.prerequisitos
|
|
1778
|
+
: t('cards.noPrerequisite')}
|
|
1779
|
+
</p>
|
|
1780
|
+
</div>
|
|
1781
|
+
</TableCell>
|
|
1782
|
+
<TableCell>
|
|
1783
|
+
<Badge variant="outline" className="text-xs">
|
|
1784
|
+
{normalizeLevelValue(formacao.nivel)}
|
|
1785
|
+
</Badge>
|
|
1786
|
+
</TableCell>
|
|
1787
|
+
<TableCell>
|
|
1788
|
+
<Badge
|
|
1789
|
+
variant={statusCfg.variant}
|
|
1790
|
+
className="text-xs"
|
|
1791
|
+
>
|
|
1792
|
+
{statusCfg.label}
|
|
1793
|
+
</Badge>
|
|
1794
|
+
</TableCell>
|
|
1795
|
+
<TableCell>{formacao.cursos.length}</TableCell>
|
|
1796
|
+
<TableCell>
|
|
821
1797
|
{formacao.cargaTotal}
|
|
822
1798
|
{t('cards.hoursLabel')}
|
|
823
|
-
</
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
{
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1799
|
+
</TableCell>
|
|
1800
|
+
<TableCell className="text-right font-medium">
|
|
1801
|
+
{formacao.alunos.toLocaleString('pt-BR')}
|
|
1802
|
+
</TableCell>
|
|
1803
|
+
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
1804
|
+
<DropdownMenu>
|
|
1805
|
+
<DropdownMenuTrigger asChild>
|
|
1806
|
+
<Button
|
|
1807
|
+
variant="ghost"
|
|
1808
|
+
size="icon"
|
|
1809
|
+
className="ml-auto size-8"
|
|
1810
|
+
aria-label={t('cards.actions.label')}
|
|
1811
|
+
>
|
|
1812
|
+
<MoreHorizontal className="size-4" />
|
|
1813
|
+
</Button>
|
|
1814
|
+
</DropdownMenuTrigger>
|
|
1815
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
1816
|
+
<DropdownMenuItem
|
|
1817
|
+
onClick={() => openEditSheet(formacao)}
|
|
1818
|
+
>
|
|
1819
|
+
<Pencil className="mr-2 size-4" />{' '}
|
|
1820
|
+
{t('cards.actions.edit')}
|
|
1821
|
+
</DropdownMenuItem>
|
|
1822
|
+
<DropdownMenuSeparator />
|
|
1823
|
+
<DropdownMenuItem
|
|
1824
|
+
className="text-destructive focus:text-destructive"
|
|
1825
|
+
onClick={(e) => openDeleteDialog(formacao, e)}
|
|
1826
|
+
>
|
|
1827
|
+
<Trash2 className="mr-2 size-4" />{' '}
|
|
1828
|
+
{t('cards.actions.delete')}
|
|
1829
|
+
</DropdownMenuItem>
|
|
1830
|
+
</DropdownMenuContent>
|
|
1831
|
+
</DropdownMenu>
|
|
1832
|
+
</TableCell>
|
|
1833
|
+
</TableRow>
|
|
1834
|
+
);
|
|
1835
|
+
})}
|
|
1836
|
+
</TableBody>
|
|
1837
|
+
</Table>
|
|
1838
|
+
</div>
|
|
1839
|
+
)}
|
|
1840
|
+
</div>
|
|
837
1841
|
)}
|
|
838
1842
|
|
|
839
1843
|
{/* Pagination footer */}
|
|
840
|
-
{!
|
|
1844
|
+
{!initialLoading && totalItems > 0 && (
|
|
841
1845
|
<div className="mt-6">
|
|
842
1846
|
<PaginationFooter
|
|
843
1847
|
currentPage={safePage}
|
|
844
1848
|
pageSize={pageSize}
|
|
845
|
-
totalItems={
|
|
1849
|
+
totalItems={totalItems}
|
|
846
1850
|
onPageChange={setCurrentPage}
|
|
847
|
-
onPageSizeChange={(
|
|
848
|
-
setPageSize(
|
|
1851
|
+
onPageSizeChange={(nextPageSize) => {
|
|
1852
|
+
setPageSize(nextPageSize);
|
|
849
1853
|
setCurrentPage(1);
|
|
850
1854
|
}}
|
|
851
1855
|
pageSizeOptions={PAGE_SIZES}
|
|
@@ -896,105 +1900,70 @@ export default function TrainingPage() {
|
|
|
896
1900
|
{form.formState.errors.descricao?.message}
|
|
897
1901
|
</FieldError>
|
|
898
1902
|
</Field>
|
|
899
|
-
<div className="grid grid-cols-
|
|
1903
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
900
1904
|
<Field>
|
|
901
1905
|
<FieldLabel>
|
|
902
|
-
{t('form.fields.
|
|
1906
|
+
{t('form.fields.nivel.label')}{' '}
|
|
903
1907
|
<span className="text-destructive">*</span>
|
|
904
1908
|
</FieldLabel>
|
|
905
1909
|
<Controller
|
|
906
|
-
name="
|
|
1910
|
+
name="nivel"
|
|
907
1911
|
control={form.control}
|
|
908
1912
|
render={({ field }) => (
|
|
909
1913
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
910
1914
|
<SelectTrigger>
|
|
911
1915
|
<SelectValue
|
|
912
|
-
placeholder={t('form.fields.
|
|
1916
|
+
placeholder={t('form.fields.nivel.placeholder')}
|
|
913
1917
|
/>
|
|
914
1918
|
</SelectTrigger>
|
|
915
1919
|
<SelectContent>
|
|
916
|
-
<SelectItem value="
|
|
917
|
-
{t('
|
|
918
|
-
</SelectItem>
|
|
919
|
-
<SelectItem value="Design">
|
|
920
|
-
{t('areas.design')}
|
|
921
|
-
</SelectItem>
|
|
922
|
-
<SelectItem value="Gestao">
|
|
923
|
-
{t('areas.management')}
|
|
1920
|
+
<SelectItem value="Iniciante">
|
|
1921
|
+
{t('levels.beginner')}
|
|
924
1922
|
</SelectItem>
|
|
925
|
-
<SelectItem value="
|
|
926
|
-
{t('
|
|
1923
|
+
<SelectItem value="Intermediario">
|
|
1924
|
+
{t('levels.intermediate')}
|
|
927
1925
|
</SelectItem>
|
|
928
|
-
<SelectItem value="
|
|
929
|
-
{t('
|
|
1926
|
+
<SelectItem value="Avancado">
|
|
1927
|
+
{t('levels.advanced')}
|
|
930
1928
|
</SelectItem>
|
|
931
1929
|
</SelectContent>
|
|
932
1930
|
</Select>
|
|
933
1931
|
)}
|
|
934
1932
|
/>
|
|
935
|
-
<FieldError>{form.formState.errors.
|
|
1933
|
+
<FieldError>{form.formState.errors.nivel?.message}</FieldError>
|
|
936
1934
|
</Field>
|
|
1935
|
+
|
|
937
1936
|
<Field>
|
|
938
1937
|
<FieldLabel>
|
|
939
|
-
{t('form.fields.
|
|
1938
|
+
{t('form.fields.status.label')}{' '}
|
|
940
1939
|
<span className="text-destructive">*</span>
|
|
941
1940
|
</FieldLabel>
|
|
942
1941
|
<Controller
|
|
943
|
-
name="
|
|
1942
|
+
name="status"
|
|
944
1943
|
control={form.control}
|
|
945
1944
|
render={({ field }) => (
|
|
946
1945
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
947
1946
|
<SelectTrigger>
|
|
948
|
-
<SelectValue
|
|
949
|
-
placeholder={t('form.fields.nivel.placeholder')}
|
|
950
|
-
/>
|
|
1947
|
+
<SelectValue />
|
|
951
1948
|
</SelectTrigger>
|
|
952
1949
|
<SelectContent>
|
|
953
|
-
<SelectItem value="
|
|
954
|
-
{t('
|
|
1950
|
+
<SelectItem value="rascunho">
|
|
1951
|
+
{t('status.draft')}
|
|
955
1952
|
</SelectItem>
|
|
956
|
-
<SelectItem value="
|
|
957
|
-
{t('
|
|
1953
|
+
<SelectItem value="ativa">
|
|
1954
|
+
{t('status.active')}
|
|
958
1955
|
</SelectItem>
|
|
959
|
-
<SelectItem value="
|
|
960
|
-
{t('
|
|
1956
|
+
<SelectItem value="encerrada">
|
|
1957
|
+
{t('status.closed')}
|
|
961
1958
|
</SelectItem>
|
|
962
1959
|
</SelectContent>
|
|
963
1960
|
</Select>
|
|
964
1961
|
)}
|
|
965
1962
|
/>
|
|
966
|
-
<FieldError>{form.formState.errors.
|
|
1963
|
+
<FieldError>{form.formState.errors.status?.message}</FieldError>
|
|
967
1964
|
</Field>
|
|
968
1965
|
</div>
|
|
969
|
-
|
|
970
|
-
<FieldLabel>
|
|
971
|
-
{t('form.fields.status.label')}{' '}
|
|
972
|
-
<span className="text-destructive">*</span>
|
|
973
|
-
</FieldLabel>
|
|
974
|
-
<Controller
|
|
975
|
-
name="status"
|
|
976
|
-
control={form.control}
|
|
977
|
-
render={({ field }) => (
|
|
978
|
-
<Select onValueChange={field.onChange} value={field.value}>
|
|
979
|
-
<SelectTrigger>
|
|
980
|
-
<SelectValue />
|
|
981
|
-
</SelectTrigger>
|
|
982
|
-
<SelectContent>
|
|
983
|
-
<SelectItem value="rascunho">
|
|
984
|
-
{t('status.draft')}
|
|
985
|
-
</SelectItem>
|
|
986
|
-
<SelectItem value="ativa">
|
|
987
|
-
{t('status.active')}
|
|
988
|
-
</SelectItem>
|
|
989
|
-
<SelectItem value="encerrada">
|
|
990
|
-
{t('status.closed')}
|
|
991
|
-
</SelectItem>
|
|
992
|
-
</SelectContent>
|
|
993
|
-
</Select>
|
|
994
|
-
)}
|
|
995
|
-
/>
|
|
996
|
-
<FieldError>{form.formState.errors.status?.message}</FieldError>
|
|
997
|
-
</Field>
|
|
1966
|
+
|
|
998
1967
|
<Field>
|
|
999
1968
|
<FieldLabel htmlFor="prerequisitos">
|
|
1000
1969
|
{t('form.fields.prerequisitos.label')}
|
|
@@ -1006,34 +1975,175 @@ export default function TrainingPage() {
|
|
|
1006
1975
|
/>
|
|
1007
1976
|
</Field>
|
|
1008
1977
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
className="flex cursor-pointer items-center justify-between border-b p-2.5 last:border-0 hover:bg-muted has-checked:bg-muted/50"
|
|
1017
|
-
>
|
|
1978
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
1979
|
+
<Field>
|
|
1980
|
+
<FieldLabel htmlFor="primaryColor">Cor Primária</FieldLabel>
|
|
1981
|
+
<Controller
|
|
1982
|
+
name="primaryColor"
|
|
1983
|
+
control={form.control}
|
|
1984
|
+
render={({ field }) => (
|
|
1018
1985
|
<div className="flex items-center gap-2">
|
|
1019
|
-
<
|
|
1020
|
-
|
|
1021
|
-
|
|
1986
|
+
<Input
|
|
1987
|
+
id="primaryColor"
|
|
1988
|
+
type="color"
|
|
1989
|
+
className="h-10 w-16 p-1"
|
|
1990
|
+
value={field.value}
|
|
1991
|
+
onChange={field.onChange}
|
|
1992
|
+
/>
|
|
1993
|
+
<Input
|
|
1994
|
+
value={field.value}
|
|
1995
|
+
onChange={field.onChange}
|
|
1996
|
+
placeholder="#1D4ED8"
|
|
1022
1997
|
/>
|
|
1023
|
-
<span className="text-sm">{c.nome}</span>
|
|
1024
1998
|
</div>
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1999
|
+
)}
|
|
2000
|
+
/>
|
|
2001
|
+
<FieldError>
|
|
2002
|
+
{form.formState.errors.primaryColor?.message}
|
|
2003
|
+
</FieldError>
|
|
2004
|
+
</Field>
|
|
2005
|
+
|
|
2006
|
+
<Field>
|
|
2007
|
+
<FieldLabel htmlFor="secondaryColor">Cor Secundária</FieldLabel>
|
|
2008
|
+
<Controller
|
|
2009
|
+
name="secondaryColor"
|
|
2010
|
+
control={form.control}
|
|
2011
|
+
render={({ field }) => (
|
|
2012
|
+
<div className="flex items-center gap-2">
|
|
2013
|
+
<Input
|
|
2014
|
+
id="secondaryColor"
|
|
2015
|
+
type="color"
|
|
2016
|
+
className="h-10 w-16 p-1"
|
|
2017
|
+
value={field.value}
|
|
2018
|
+
onChange={field.onChange}
|
|
2019
|
+
/>
|
|
2020
|
+
<Input
|
|
2021
|
+
value={field.value}
|
|
2022
|
+
onChange={field.onChange}
|
|
2023
|
+
placeholder="#111827"
|
|
2024
|
+
/>
|
|
2025
|
+
</div>
|
|
2026
|
+
)}
|
|
2027
|
+
/>
|
|
2028
|
+
<FieldError>
|
|
2029
|
+
{form.formState.errors.secondaryColor?.message}
|
|
2030
|
+
</FieldError>
|
|
2031
|
+
</Field>
|
|
2032
|
+
</div>
|
|
2033
|
+
|
|
2034
|
+
{/* Trilha */}
|
|
2035
|
+
<Field>
|
|
2036
|
+
<FieldLabel>{t('form.fields.trilha.label')}</FieldLabel>
|
|
2037
|
+
<div className="space-y-2">
|
|
2038
|
+
<div className="rounded-md border bg-muted/20 p-3">
|
|
2039
|
+
<div className="space-y-2">
|
|
2040
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
|
|
2041
|
+
<Select
|
|
2042
|
+
value={selectedCourseToAdd}
|
|
2043
|
+
onValueChange={handleCourseSelection}
|
|
2044
|
+
disabled={selectableCursos.length === 0}
|
|
2045
|
+
>
|
|
2046
|
+
<SelectTrigger className="w-full">
|
|
2047
|
+
<SelectValue
|
|
2048
|
+
placeholder={t('form.fields.cursos.placeholder')}
|
|
2049
|
+
/>
|
|
2050
|
+
</SelectTrigger>
|
|
2051
|
+
<SelectContent>
|
|
2052
|
+
{selectableCursos.map((course) => (
|
|
2053
|
+
<SelectItem
|
|
2054
|
+
key={course.id}
|
|
2055
|
+
value={String(course.id)}
|
|
2056
|
+
>
|
|
2057
|
+
{course.nome} ({course.cargaHoraria}h)
|
|
2058
|
+
</SelectItem>
|
|
2059
|
+
))}
|
|
2060
|
+
</SelectContent>
|
|
2061
|
+
</Select>
|
|
2062
|
+
<Button
|
|
2063
|
+
type="button"
|
|
2064
|
+
variant="outline"
|
|
2065
|
+
className="w-full shrink-0 whitespace-nowrap sm:w-auto"
|
|
2066
|
+
onClick={openCreateCourseSheet}
|
|
2067
|
+
>
|
|
2068
|
+
<Plus className="size-4" />
|
|
2069
|
+
</Button>
|
|
2070
|
+
</div>
|
|
2071
|
+
|
|
2072
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
|
|
2073
|
+
<Select
|
|
2074
|
+
value={selectedExamToAdd}
|
|
2075
|
+
onValueChange={handleExamSelection}
|
|
2076
|
+
disabled={selectableExams.length === 0}
|
|
2077
|
+
>
|
|
2078
|
+
<SelectTrigger className="w-full">
|
|
2079
|
+
<SelectValue
|
|
2080
|
+
placeholder={t('form.fields.exames.placeholder')}
|
|
2081
|
+
/>
|
|
2082
|
+
</SelectTrigger>
|
|
2083
|
+
<SelectContent>
|
|
2084
|
+
{selectableExams.map((exam) => (
|
|
2085
|
+
<SelectItem key={exam.id} value={String(exam.id)}>
|
|
2086
|
+
{exam.titulo} ({exam.limiteTempo}min)
|
|
2087
|
+
</SelectItem>
|
|
2088
|
+
))}
|
|
2089
|
+
</SelectContent>
|
|
2090
|
+
</Select>
|
|
2091
|
+
<Button
|
|
2092
|
+
type="button"
|
|
2093
|
+
variant="outline"
|
|
2094
|
+
className="w-full shrink-0 whitespace-nowrap sm:w-auto"
|
|
2095
|
+
onClick={openCreateExamSheet}
|
|
2096
|
+
>
|
|
2097
|
+
<Plus className="size-4" />
|
|
2098
|
+
</Button>
|
|
2099
|
+
</div>
|
|
2100
|
+
</div>
|
|
2101
|
+
</div>
|
|
2102
|
+
|
|
2103
|
+
{trailItems.length > 0 ? (
|
|
2104
|
+
<>
|
|
2105
|
+
<div className="rounded-md border bg-background">
|
|
2106
|
+
<DndContext
|
|
2107
|
+
sensors={sensors}
|
|
2108
|
+
collisionDetection={closestCenter}
|
|
2109
|
+
onDragEnd={handleTrailDragEnd}
|
|
2110
|
+
>
|
|
2111
|
+
<SortableContext
|
|
2112
|
+
items={trailItems.map((item) => item.uid)}
|
|
2113
|
+
strategy={verticalListSortingStrategy}
|
|
2114
|
+
>
|
|
2115
|
+
{trailItems.map((item) => (
|
|
2116
|
+
<SortableTrailItem
|
|
2117
|
+
key={item.uid}
|
|
2118
|
+
item={item}
|
|
2119
|
+
onRemove={removeTrailItem}
|
|
2120
|
+
/>
|
|
2121
|
+
))}
|
|
2122
|
+
</SortableContext>
|
|
2123
|
+
</DndContext>
|
|
2124
|
+
</div>
|
|
2125
|
+
</>
|
|
2126
|
+
) : (
|
|
2127
|
+
<div className="rounded-md border p-3">
|
|
2128
|
+
<p className="text-sm text-muted-foreground">
|
|
2129
|
+
{t('form.fields.trilha.empty')}
|
|
2130
|
+
</p>
|
|
2131
|
+
</div>
|
|
2132
|
+
)}
|
|
1030
2133
|
</div>
|
|
1031
|
-
|
|
2134
|
+
|
|
2135
|
+
{(isFetchingCourses || isFetchingExams) && (
|
|
2136
|
+
<p className="text-xs text-muted-foreground">
|
|
2137
|
+
{t('form.fields.trilha.loading')}
|
|
2138
|
+
</p>
|
|
2139
|
+
)}
|
|
2140
|
+
|
|
2141
|
+
{learningPathItems.length > 0 && (
|
|
1032
2142
|
<p className="text-xs text-muted-foreground mt-1">
|
|
1033
|
-
{
|
|
2143
|
+
{learningPathItems.length} {t('coursesSummary.items')}{' '}
|
|
1034
2144
|
{t('coursesSummary.dot')}{' '}
|
|
1035
2145
|
{availableCursos
|
|
1036
|
-
.filter((c) => selectedCursos.includes(c.
|
|
2146
|
+
.filter((c) => selectedCursos.includes(c.id))
|
|
1037
2147
|
.reduce((a, c) => a + c.cargaHoraria, 0)}
|
|
1038
2148
|
{t('coursesSummary.hours')}
|
|
1039
2149
|
</p>
|
|
@@ -1041,8 +2151,14 @@ export default function TrainingPage() {
|
|
|
1041
2151
|
</Field>
|
|
1042
2152
|
|
|
1043
2153
|
<SheetFooter className="mt-auto shrink-0 gap-2 pt-4 px-0">
|
|
1044
|
-
<Button
|
|
1045
|
-
|
|
2154
|
+
<Button
|
|
2155
|
+
type="submit"
|
|
2156
|
+
disabled={saving || loadingEditSheet}
|
|
2157
|
+
className="gap-2"
|
|
2158
|
+
>
|
|
2159
|
+
{(saving || loadingEditSheet) && (
|
|
2160
|
+
<Loader2 className="size-4 animate-spin" />
|
|
2161
|
+
)}
|
|
1046
2162
|
{editingFormacao
|
|
1047
2163
|
? t('form.actions.save')
|
|
1048
2164
|
: t('form.actions.create')}
|
|
@@ -1052,9 +2168,138 @@ export default function TrainingPage() {
|
|
|
1052
2168
|
</SheetContent>
|
|
1053
2169
|
</Sheet>
|
|
1054
2170
|
|
|
2171
|
+
<CourseFormSheet
|
|
2172
|
+
open={courseSheetOpen}
|
|
2173
|
+
onOpenChange={setCourseSheetOpen}
|
|
2174
|
+
editing={false}
|
|
2175
|
+
saving={creatingCourse}
|
|
2176
|
+
form={courseForm}
|
|
2177
|
+
onSubmit={onSubmitCourse}
|
|
2178
|
+
categories={categoryOptions}
|
|
2179
|
+
onCreateCategory={() => router.push('/category?new=1')}
|
|
2180
|
+
t={tCourse}
|
|
2181
|
+
/>
|
|
2182
|
+
|
|
2183
|
+
<Sheet open={examSheetOpen} onOpenChange={setExamSheetOpen}>
|
|
2184
|
+
<SheetContent
|
|
2185
|
+
side="right"
|
|
2186
|
+
className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
|
|
2187
|
+
>
|
|
2188
|
+
<SheetHeader>
|
|
2189
|
+
<SheetTitle>{t('examForm.title')}</SheetTitle>
|
|
2190
|
+
<SheetDescription>{t('examForm.description')}</SheetDescription>
|
|
2191
|
+
</SheetHeader>
|
|
2192
|
+
|
|
2193
|
+
<form
|
|
2194
|
+
onSubmit={examForm.handleSubmit(onSubmitExam)}
|
|
2195
|
+
className="flex flex-1 flex-col gap-4 px-4 py-6"
|
|
2196
|
+
>
|
|
2197
|
+
<Field>
|
|
2198
|
+
<FieldLabel htmlFor="exam-titulo">
|
|
2199
|
+
{tExam('form.fields.title.label')}{' '}
|
|
2200
|
+
<span className="text-destructive">*</span>
|
|
2201
|
+
</FieldLabel>
|
|
2202
|
+
<Input
|
|
2203
|
+
id="exam-titulo"
|
|
2204
|
+
placeholder={tExam('form.fields.title.placeholder')}
|
|
2205
|
+
{...examForm.register('titulo')}
|
|
2206
|
+
/>
|
|
2207
|
+
<FieldError>
|
|
2208
|
+
{examForm.formState.errors.titulo?.message}
|
|
2209
|
+
</FieldError>
|
|
2210
|
+
</Field>
|
|
2211
|
+
|
|
2212
|
+
<div className="grid grid-cols-2 gap-4">
|
|
2213
|
+
<Field>
|
|
2214
|
+
<FieldLabel htmlFor="exam-min-score">
|
|
2215
|
+
{tExam('form.fields.minScore.label')}{' '}
|
|
2216
|
+
<span className="text-destructive">*</span>
|
|
2217
|
+
</FieldLabel>
|
|
2218
|
+
<Input
|
|
2219
|
+
id="exam-min-score"
|
|
2220
|
+
type="number"
|
|
2221
|
+
step="0.5"
|
|
2222
|
+
{...examForm.register('notaMinima')}
|
|
2223
|
+
/>
|
|
2224
|
+
</Field>
|
|
2225
|
+
|
|
2226
|
+
<Field>
|
|
2227
|
+
<FieldLabel htmlFor="exam-time-limit">
|
|
2228
|
+
{tExam('form.fields.timeLimit.label')}{' '}
|
|
2229
|
+
<span className="text-destructive">*</span>
|
|
2230
|
+
</FieldLabel>
|
|
2231
|
+
<Input
|
|
2232
|
+
id="exam-time-limit"
|
|
2233
|
+
type="number"
|
|
2234
|
+
{...examForm.register('limiteTempo')}
|
|
2235
|
+
/>
|
|
2236
|
+
</Field>
|
|
2237
|
+
</div>
|
|
2238
|
+
|
|
2239
|
+
<Field>
|
|
2240
|
+
<FieldLabel>{tExam('form.fields.status.label')}</FieldLabel>
|
|
2241
|
+
<Controller
|
|
2242
|
+
name="status"
|
|
2243
|
+
control={examForm.control}
|
|
2244
|
+
render={({ field }) => (
|
|
2245
|
+
<Select onValueChange={field.onChange} value={field.value}>
|
|
2246
|
+
<SelectTrigger>
|
|
2247
|
+
<SelectValue
|
|
2248
|
+
placeholder={tExam('form.fields.status.placeholder')}
|
|
2249
|
+
/>
|
|
2250
|
+
</SelectTrigger>
|
|
2251
|
+
<SelectContent>
|
|
2252
|
+
<SelectItem value="rascunho">
|
|
2253
|
+
{tExam('status.draft')}
|
|
2254
|
+
</SelectItem>
|
|
2255
|
+
<SelectItem value="publicado">
|
|
2256
|
+
{tExam('status.published')}
|
|
2257
|
+
</SelectItem>
|
|
2258
|
+
<SelectItem value="encerrado">
|
|
2259
|
+
{tExam('status.closed')}
|
|
2260
|
+
</SelectItem>
|
|
2261
|
+
</SelectContent>
|
|
2262
|
+
</Select>
|
|
2263
|
+
)}
|
|
2264
|
+
/>
|
|
2265
|
+
</Field>
|
|
2266
|
+
|
|
2267
|
+
<Field>
|
|
2268
|
+
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
2269
|
+
<div>
|
|
2270
|
+
<p className="text-sm font-medium">
|
|
2271
|
+
{tExam('form.fields.shuffle.label')}
|
|
2272
|
+
</p>
|
|
2273
|
+
<p className="text-xs text-muted-foreground">
|
|
2274
|
+
{tExam('form.fields.shuffle.description')}
|
|
2275
|
+
</p>
|
|
2276
|
+
</div>
|
|
2277
|
+
<Controller
|
|
2278
|
+
name="shuffle"
|
|
2279
|
+
control={examForm.control}
|
|
2280
|
+
render={({ field }) => (
|
|
2281
|
+
<Switch
|
|
2282
|
+
checked={field.value}
|
|
2283
|
+
onCheckedChange={field.onChange}
|
|
2284
|
+
/>
|
|
2285
|
+
)}
|
|
2286
|
+
/>
|
|
2287
|
+
</div>
|
|
2288
|
+
</Field>
|
|
2289
|
+
|
|
2290
|
+
<SheetFooter className="mt-auto px-0">
|
|
2291
|
+
<Button type="submit" disabled={creatingExam} className="gap-2">
|
|
2292
|
+
{creatingExam && <Loader2 className="size-4 animate-spin" />}
|
|
2293
|
+
{t('examForm.actions.create')}
|
|
2294
|
+
</Button>
|
|
2295
|
+
</SheetFooter>
|
|
2296
|
+
</form>
|
|
2297
|
+
</SheetContent>
|
|
2298
|
+
</Sheet>
|
|
2299
|
+
|
|
1055
2300
|
{/* Delete Dialog */}
|
|
1056
2301
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
1057
|
-
<DialogContent>
|
|
2302
|
+
<DialogContent className="max-w-3xl">
|
|
1058
2303
|
<DialogHeader>
|
|
1059
2304
|
<DialogTitle className="flex items-center gap-2">
|
|
1060
2305
|
<AlertTriangle className="size-5 text-destructive" />{' '}
|