@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,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
4
|
import { Badge } from '@/components/ui/badge';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
6
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
DialogHeader,
|
|
13
13
|
DialogTitle,
|
|
14
14
|
} from '@/components/ui/dialog';
|
|
15
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
15
16
|
import { Progress } from '@/components/ui/progress';
|
|
17
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
18
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
16
19
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
17
20
|
import {
|
|
18
21
|
AlertTriangle,
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
} from 'lucide-react';
|
|
31
34
|
import { useTranslations } from 'next-intl';
|
|
32
35
|
import Link from 'next/link';
|
|
36
|
+
import { useParams } from 'next/navigation';
|
|
33
37
|
import { useEffect, useRef, useState } from 'react';
|
|
34
38
|
import { toast } from 'sonner';
|
|
35
39
|
|
|
@@ -214,22 +218,101 @@ const examQuestions: Questao[] = [
|
|
|
214
218
|
const EXAM_TITLE = 'Prova Final - React Avancado';
|
|
215
219
|
const EXAM_TIME_MINUTES = 120;
|
|
216
220
|
|
|
221
|
+
interface ExamInfo {
|
|
222
|
+
id: number;
|
|
223
|
+
title: string;
|
|
224
|
+
questionCount: number;
|
|
225
|
+
maxPoints: number;
|
|
226
|
+
timeLimitMinutes: number | null;
|
|
227
|
+
passingScore: number;
|
|
228
|
+
passingPercent: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface AttemptsInfo {
|
|
232
|
+
allowed: number;
|
|
233
|
+
used: number;
|
|
234
|
+
remaining: number;
|
|
235
|
+
canStart: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface AttemptResult {
|
|
239
|
+
totalPoints: number;
|
|
240
|
+
maxPoints: number;
|
|
241
|
+
score: number;
|
|
242
|
+
scoreDisplay: number;
|
|
243
|
+
percent: number;
|
|
244
|
+
passed: boolean | null;
|
|
245
|
+
hasPendingReview: boolean;
|
|
246
|
+
pendingReviewCount: number;
|
|
247
|
+
answersSummary: {
|
|
248
|
+
questionId: number;
|
|
249
|
+
hasAnswer: boolean;
|
|
250
|
+
isCorrect: boolean | null;
|
|
251
|
+
pointsAwarded: number;
|
|
252
|
+
requiresManualReview: boolean;
|
|
253
|
+
}[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
type ExamQuestionType =
|
|
257
|
+
| 'multiple_choice'
|
|
258
|
+
| 'true_false'
|
|
259
|
+
| 'essay'
|
|
260
|
+
| 'fill_blank'
|
|
261
|
+
| 'matching';
|
|
262
|
+
|
|
263
|
+
interface ExamQuestion {
|
|
264
|
+
id: number;
|
|
265
|
+
order: number;
|
|
266
|
+
questionType: ExamQuestionType;
|
|
267
|
+
statement: string;
|
|
268
|
+
points: number;
|
|
269
|
+
alternatives: { id: number; text: string }[];
|
|
270
|
+
fillBlankAnswers?: Array<{ answer: string; alternatives?: string[] }>;
|
|
271
|
+
matchingPairs?: Array<{ id: string; leftText: string; rightText: string }>;
|
|
272
|
+
matchingOptions?: Array<{ id: string; text: string }>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type ExamAnswerValue = {
|
|
276
|
+
examOptionId: number | null;
|
|
277
|
+
answerText: string | null;
|
|
278
|
+
matchingPairs: Array<{ leftId: string; rightId: string }>;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
interface ExamAttemptState {
|
|
282
|
+
exam: ExamInfo;
|
|
283
|
+
attempts: AttemptsInfo;
|
|
284
|
+
questions: ExamQuestion[];
|
|
285
|
+
attempt: {
|
|
286
|
+
id: number;
|
|
287
|
+
status: string;
|
|
288
|
+
timeRemainingSeconds: number | null;
|
|
289
|
+
answeredCount: number;
|
|
290
|
+
answers: {
|
|
291
|
+
questionId: number;
|
|
292
|
+
examOptionId: number | null;
|
|
293
|
+
answerText: string | null;
|
|
294
|
+
matchingPairs: Array<{ leftId: string; rightId: string }>;
|
|
295
|
+
}[];
|
|
296
|
+
result: AttemptResult | null;
|
|
297
|
+
} | null;
|
|
298
|
+
}
|
|
299
|
+
|
|
217
300
|
export default function TentativaPage() {
|
|
218
301
|
const t = useTranslations('lms.AttemptPage');
|
|
302
|
+
const { id } = useParams<{ id: string }>();
|
|
303
|
+
const examId = Number(id);
|
|
304
|
+
const { request } = useApp();
|
|
305
|
+
const [examState, setExamState] = useState<ExamAttemptState | null>(null);
|
|
306
|
+
const [isLoadingExam, setIsLoadingExam] = useState(true);
|
|
219
307
|
const [started, setStarted] = useState(false);
|
|
220
308
|
const [finished, setFinished] = useState(false);
|
|
221
309
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
222
|
-
const [answers, setAnswers] = useState<Record<string,
|
|
310
|
+
const [answers, setAnswers] = useState<Record<string, ExamAnswerValue>>({});
|
|
223
311
|
const [flagged, setFlagged] = useState<Set<string>>(new Set());
|
|
224
312
|
const [timeLeft, setTimeLeft] = useState(EXAM_TIME_MINUTES * 60);
|
|
225
313
|
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
|
|
226
314
|
const [timeUpDialogOpen, setTimeUpDialogOpen] = useState(false);
|
|
227
315
|
const [showNav, setShowNav] = useState(false);
|
|
228
|
-
const [score, setScore] = useState<{
|
|
229
|
-
total: number;
|
|
230
|
-
max: number;
|
|
231
|
-
percent: number;
|
|
232
|
-
} | null>(null);
|
|
233
316
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
234
317
|
|
|
235
318
|
// Timer
|
|
@@ -255,15 +338,90 @@ export default function TentativaPage() {
|
|
|
255
338
|
useEffect(() => {
|
|
256
339
|
if (started && !finished && Object.keys(answers).length > 0) {
|
|
257
340
|
const timeout = setTimeout(() => {
|
|
258
|
-
|
|
341
|
+
saveAnswers();
|
|
259
342
|
}, 2000);
|
|
260
343
|
return () => clearTimeout(timeout);
|
|
261
344
|
}
|
|
262
345
|
}, [answers, started, finished]);
|
|
263
346
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
347
|
+
function isObjectiveQuestion(questionType?: ExamQuestionType) {
|
|
348
|
+
return questionType === 'multiple_choice' || questionType === 'true_false';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function hasAnswerValue(answer?: ExamAnswerValue) {
|
|
352
|
+
if (!answer) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (answer.examOptionId != null) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if ((answer.answerText ?? '').trim().length > 0) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return (answer.matchingPairs ?? []).length > 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function mapAnswersPayload() {
|
|
368
|
+
return Object.entries(answers)
|
|
369
|
+
.filter(([, answer]) => hasAnswerValue(answer))
|
|
370
|
+
.map(([questionId, answer]) => ({
|
|
371
|
+
questionId: Number(questionId),
|
|
372
|
+
examOptionId: answer.examOptionId,
|
|
373
|
+
answerText: answer.answerText?.trim() || undefined,
|
|
374
|
+
matchingPairs: answer.matchingPairs,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Load exam state on mount
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
async function load() {
|
|
381
|
+
try {
|
|
382
|
+
const { data } = await request<ExamAttemptState>({
|
|
383
|
+
url: `/lms/exams/${examId}/attempt`,
|
|
384
|
+
method: 'GET',
|
|
385
|
+
});
|
|
386
|
+
setExamState(data);
|
|
387
|
+
if (data.attempt?.timeRemainingSeconds != null) {
|
|
388
|
+
setTimeLeft(data.attempt.timeRemainingSeconds);
|
|
389
|
+
}
|
|
390
|
+
if (data.attempt?.answers) {
|
|
391
|
+
const restoredAnswers: Record<string, ExamAnswerValue> = {};
|
|
392
|
+
for (const answer of data.attempt.answers) {
|
|
393
|
+
restoredAnswers[String(answer.questionId)] = {
|
|
394
|
+
examOptionId: answer.examOptionId,
|
|
395
|
+
answerText: answer.answerText,
|
|
396
|
+
matchingPairs: answer.matchingPairs ?? [],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
setAnswers(restoredAnswers);
|
|
400
|
+
}
|
|
401
|
+
if (data.attempt?.status === 'in_progress') {
|
|
402
|
+
setStarted(true);
|
|
403
|
+
}
|
|
404
|
+
} finally {
|
|
405
|
+
setIsLoadingExam(false);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
load();
|
|
409
|
+
}, [examId]);
|
|
410
|
+
|
|
411
|
+
const apiQuestions = examState?.questions ?? [];
|
|
412
|
+
const currentQuestion = apiQuestions[currentIndex];
|
|
413
|
+
const answeredCount = Object.values(answers).filter((answer) =>
|
|
414
|
+
hasAnswerValue(answer)
|
|
415
|
+
).length;
|
|
416
|
+
const progressPercent =
|
|
417
|
+
apiQuestions.length > 0 ? (answeredCount / apiQuestions.length) * 100 : 0;
|
|
418
|
+
const hasCompletedAttempt = examState?.attempt?.status === 'completed';
|
|
419
|
+
const hasActiveAttempt = examState?.attempt?.status === 'in_progress';
|
|
420
|
+
const hasRemainingAttempts = examState?.attempts.canStart !== false;
|
|
421
|
+
const shouldShowCompletedNotice =
|
|
422
|
+
hasCompletedAttempt &&
|
|
423
|
+
!hasRemainingAttempts &&
|
|
424
|
+
Boolean(examState?.attempt?.result);
|
|
267
425
|
|
|
268
426
|
if (!currentQuestion) {
|
|
269
427
|
return (
|
|
@@ -298,7 +456,47 @@ export default function TentativaPage() {
|
|
|
298
456
|
}
|
|
299
457
|
|
|
300
458
|
function selectAnswer(questionId: string, alternativaId: string) {
|
|
301
|
-
setAnswers((prev) => ({
|
|
459
|
+
setAnswers((prev) => ({
|
|
460
|
+
...prev,
|
|
461
|
+
[questionId]: {
|
|
462
|
+
examOptionId: Number(alternativaId),
|
|
463
|
+
answerText: null,
|
|
464
|
+
matchingPairs: [],
|
|
465
|
+
},
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function updateTextAnswer(questionId: string, answerText: string) {
|
|
470
|
+
setAnswers((prev) => ({
|
|
471
|
+
...prev,
|
|
472
|
+
[questionId]: {
|
|
473
|
+
examOptionId: null,
|
|
474
|
+
answerText,
|
|
475
|
+
matchingPairs: [],
|
|
476
|
+
},
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function updateMatchingAnswer(
|
|
481
|
+
questionId: string,
|
|
482
|
+
leftId: string,
|
|
483
|
+
rightId: string
|
|
484
|
+
) {
|
|
485
|
+
setAnswers((prev) => {
|
|
486
|
+
const currentPairs = prev[questionId]?.matchingPairs ?? [];
|
|
487
|
+
const nextPairs = currentPairs
|
|
488
|
+
.filter((pair) => pair.leftId !== leftId)
|
|
489
|
+
.concat(rightId ? [{ leftId, rightId }] : []);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
...prev,
|
|
493
|
+
[questionId]: {
|
|
494
|
+
examOptionId: null,
|
|
495
|
+
answerText: null,
|
|
496
|
+
matchingPairs: nextPairs,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
});
|
|
302
500
|
}
|
|
303
501
|
|
|
304
502
|
function toggleFlag(questionId: string) {
|
|
@@ -315,35 +513,104 @@ export default function TentativaPage() {
|
|
|
315
513
|
setShowNav(false);
|
|
316
514
|
}
|
|
317
515
|
|
|
318
|
-
function
|
|
319
|
-
|
|
516
|
+
async function saveAnswers() {
|
|
517
|
+
const attemptId = examState?.attempt?.id;
|
|
518
|
+
if (!attemptId) return;
|
|
519
|
+
await request({
|
|
520
|
+
url: `/lms/exams/${examId}/attempt/${attemptId}/answers`,
|
|
521
|
+
method: 'PATCH',
|
|
522
|
+
data: {
|
|
523
|
+
answers: mapAnswersPayload(),
|
|
524
|
+
},
|
|
525
|
+
});
|
|
320
526
|
}
|
|
321
527
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
examQuestions.forEach((q) => {
|
|
326
|
-
max += q.pontuacao;
|
|
327
|
-
const selectedAltId = answers[q.id];
|
|
328
|
-
if (selectedAltId) {
|
|
329
|
-
const alt = q.alternativas.find((a) => a.id === selectedAltId);
|
|
330
|
-
if (alt?.correta) total += q.pontuacao;
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
return { total, max, percent: Math.round((total / max) * 100) };
|
|
528
|
+
async function handleSaveProgress() {
|
|
529
|
+
await saveAnswers();
|
|
530
|
+
toast.success(t('examInProgress.progressSaved'));
|
|
334
531
|
}
|
|
335
532
|
|
|
336
|
-
function handleSubmit() {
|
|
533
|
+
async function handleSubmit(force = false) {
|
|
337
534
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
338
|
-
const result = calculateScore();
|
|
339
|
-
setScore(result);
|
|
340
|
-
setFinished(true);
|
|
341
535
|
setSubmitDialogOpen(false);
|
|
342
536
|
setTimeUpDialogOpen(false);
|
|
537
|
+
try {
|
|
538
|
+
const attemptId = examState?.attempt?.id;
|
|
539
|
+
if (!attemptId) return;
|
|
540
|
+
const { data } = await request<ExamAttemptState>({
|
|
541
|
+
url: `/lms/exams/${examId}/attempt/${attemptId}/submit`,
|
|
542
|
+
method: 'POST',
|
|
543
|
+
data: {
|
|
544
|
+
force,
|
|
545
|
+
answers: mapAnswersPayload(),
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
setExamState(data);
|
|
549
|
+
setFinished(true);
|
|
550
|
+
} catch {
|
|
551
|
+
toast.error(t('examInProgress.submitError'));
|
|
552
|
+
}
|
|
343
553
|
}
|
|
344
554
|
|
|
345
555
|
function handleForceSubmit() {
|
|
346
|
-
handleSubmit();
|
|
556
|
+
handleSubmit(true);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function handleStartAttempt() {
|
|
560
|
+
const { data } = await request<ExamAttemptState>({
|
|
561
|
+
url: `/lms/exams/${examId}/attempt/start`,
|
|
562
|
+
method: 'POST',
|
|
563
|
+
data: {},
|
|
564
|
+
});
|
|
565
|
+
setExamState(data);
|
|
566
|
+
if (data.attempt?.timeRemainingSeconds != null) {
|
|
567
|
+
setTimeLeft(data.attempt.timeRemainingSeconds);
|
|
568
|
+
}
|
|
569
|
+
if (data.attempt?.answers) {
|
|
570
|
+
const restoredAnswers: Record<string, ExamAnswerValue> = {};
|
|
571
|
+
for (const a of data.attempt.answers) {
|
|
572
|
+
restoredAnswers[String(a.questionId)] = {
|
|
573
|
+
examOptionId: a.examOptionId,
|
|
574
|
+
answerText: a.answerText,
|
|
575
|
+
matchingPairs: a.matchingPairs ?? [],
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
setAnswers(restoredAnswers);
|
|
579
|
+
}
|
|
580
|
+
setStarted(true);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Loading screen
|
|
584
|
+
if (isLoadingExam) {
|
|
585
|
+
return (
|
|
586
|
+
<Page>
|
|
587
|
+
<PageHeader
|
|
588
|
+
title={t('breadcrumbs.attemptExam')}
|
|
589
|
+
breadcrumbs={[
|
|
590
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
591
|
+
{ label: t('breadcrumbs.exams'), href: '/lms/exams' },
|
|
592
|
+
{ label: t('breadcrumbs.attemptExam') },
|
|
593
|
+
]}
|
|
594
|
+
/>
|
|
595
|
+
<div className="flex justify-center">
|
|
596
|
+
<Card className="w-full max-w-3xl animate-pulse border-border/70">
|
|
597
|
+
<CardHeader className="text-center">
|
|
598
|
+
<div className="mx-auto mb-4 size-16 rounded-2xl bg-muted" />
|
|
599
|
+
<div className="mx-auto h-7 w-48 rounded bg-muted" />
|
|
600
|
+
</CardHeader>
|
|
601
|
+
<CardContent className="flex flex-col gap-4">
|
|
602
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
603
|
+
{[...Array(4)].map((_, i) => (
|
|
604
|
+
<div key={i} className="h-16 rounded-lg bg-muted" />
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
<div className="h-32 rounded-lg bg-muted" />
|
|
608
|
+
<div className="h-10 rounded bg-muted" />
|
|
609
|
+
</CardContent>
|
|
610
|
+
</Card>
|
|
611
|
+
</div>
|
|
612
|
+
</Page>
|
|
613
|
+
);
|
|
347
614
|
}
|
|
348
615
|
|
|
349
616
|
// Start screen
|
|
@@ -351,6 +618,8 @@ export default function TentativaPage() {
|
|
|
351
618
|
return (
|
|
352
619
|
<Page>
|
|
353
620
|
<PageHeader
|
|
621
|
+
title={examState?.exam.title ?? EXAM_TITLE}
|
|
622
|
+
description={t('breadcrumbs.attemptExam')}
|
|
354
623
|
breadcrumbs={[
|
|
355
624
|
{
|
|
356
625
|
label: t('breadcrumbs.home'),
|
|
@@ -370,45 +639,71 @@ export default function TentativaPage() {
|
|
|
370
639
|
initial={{ opacity: 0, y: 20 }}
|
|
371
640
|
animate={{ opacity: 1, y: 0 }}
|
|
372
641
|
transition={{ duration: 0.5 }}
|
|
642
|
+
className="w-full max-w-4xl"
|
|
373
643
|
>
|
|
374
|
-
<Card className="
|
|
644
|
+
<Card className="border-border/70">
|
|
375
645
|
<CardHeader className="text-center">
|
|
376
|
-
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl bg-orange-
|
|
377
|
-
<GraduationCap className="size-8
|
|
646
|
+
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl bg-orange-100 text-orange-700">
|
|
647
|
+
<GraduationCap className="size-8" />
|
|
378
648
|
</div>
|
|
379
|
-
<CardTitle className="text-2xl">
|
|
649
|
+
<CardTitle className="text-2xl">
|
|
650
|
+
{examState?.exam.title ?? EXAM_TITLE}
|
|
651
|
+
</CardTitle>
|
|
380
652
|
</CardHeader>
|
|
381
|
-
<CardContent className="flex flex-col gap-
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
653
|
+
<CardContent className="flex flex-col gap-5">
|
|
654
|
+
<KpiCardsGrid
|
|
655
|
+
items={[
|
|
656
|
+
{
|
|
657
|
+
key: 'questions',
|
|
658
|
+
title: t('startScreen.questions'),
|
|
659
|
+
value:
|
|
660
|
+
examState?.exam.questionCount ?? examQuestions.length,
|
|
661
|
+
icon: GraduationCap,
|
|
662
|
+
layout: 'compact',
|
|
663
|
+
accentClassName:
|
|
664
|
+
'from-orange-500/25 via-amber-500/15 to-transparent',
|
|
665
|
+
iconContainerClassName: 'bg-orange-100 text-orange-700',
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
key: 'minutes',
|
|
669
|
+
title: t('startScreen.minutes'),
|
|
670
|
+
value:
|
|
671
|
+
examState?.exam.timeLimitMinutes ?? EXAM_TIME_MINUTES,
|
|
672
|
+
icon: Clock,
|
|
673
|
+
layout: 'compact',
|
|
674
|
+
accentClassName:
|
|
675
|
+
'from-sky-500/25 via-blue-500/15 to-transparent',
|
|
676
|
+
iconContainerClassName: 'bg-sky-100 text-sky-700',
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
key: 'points',
|
|
680
|
+
title: t('startScreen.points'),
|
|
681
|
+
value:
|
|
682
|
+
examState?.exam.maxPoints ??
|
|
683
|
+
examQuestions.reduce((a, q) => a + q.pontuacao, 0),
|
|
684
|
+
icon: CheckCircle2,
|
|
685
|
+
layout: 'compact',
|
|
686
|
+
accentClassName:
|
|
687
|
+
'from-emerald-500/25 via-green-500/15 to-transparent',
|
|
688
|
+
iconContainerClassName: 'bg-emerald-100 text-emerald-700',
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
key: 'min-grade',
|
|
692
|
+
title: t('startScreen.minGrade'),
|
|
693
|
+
value:
|
|
694
|
+
examState != null
|
|
695
|
+
? examState.exam.passingScore.toFixed(1)
|
|
696
|
+
: '7.0',
|
|
697
|
+
icon: BookmarkCheck,
|
|
698
|
+
layout: 'compact',
|
|
699
|
+
accentClassName:
|
|
700
|
+
'from-violet-500/25 via-indigo-500/15 to-transparent',
|
|
701
|
+
iconContainerClassName: 'bg-violet-100 text-violet-700',
|
|
702
|
+
},
|
|
703
|
+
]}
|
|
704
|
+
/>
|
|
410
705
|
|
|
411
|
-
<div className="rounded-
|
|
706
|
+
<div className="rounded-xl border border-border/70 p-5">
|
|
412
707
|
<h3 className="mb-2 text-sm font-medium">
|
|
413
708
|
{t('startScreen.instructions')}
|
|
414
709
|
</h3>
|
|
@@ -421,14 +716,39 @@ export default function TentativaPage() {
|
|
|
421
716
|
</ul>
|
|
422
717
|
</div>
|
|
423
718
|
|
|
424
|
-
|
|
425
|
-
<
|
|
426
|
-
className="
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
719
|
+
{shouldShowCompletedNotice ? (
|
|
720
|
+
<div className="rounded-xl border border-primary/20 bg-primary/5 p-5">
|
|
721
|
+
<p className="text-sm font-semibold text-primary">
|
|
722
|
+
VOCE JA REALIZOU ESSE TESTE
|
|
723
|
+
</p>
|
|
724
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
725
|
+
Sua tentativa ja foi concluida e nao ha novas tentativas
|
|
726
|
+
disponiveis.
|
|
727
|
+
</p>
|
|
728
|
+
<div className="mt-4 flex gap-3">
|
|
729
|
+
<Button
|
|
730
|
+
className="flex-1 gap-2"
|
|
731
|
+
onClick={() => {
|
|
732
|
+
setStarted(true);
|
|
733
|
+
setFinished(true);
|
|
734
|
+
}}
|
|
735
|
+
>
|
|
736
|
+
VEJA SEU RESULTADO
|
|
737
|
+
</Button>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
) : (
|
|
741
|
+
<div className="flex gap-3">
|
|
742
|
+
<Button
|
|
743
|
+
className="flex-1 gap-2"
|
|
744
|
+
onClick={handleStartAttempt}
|
|
745
|
+
>
|
|
746
|
+
{hasActiveAttempt
|
|
747
|
+
? 'Continuar teste'
|
|
748
|
+
: t('startScreen.startButton')}
|
|
749
|
+
</Button>
|
|
750
|
+
</div>
|
|
751
|
+
)}
|
|
432
752
|
</CardContent>
|
|
433
753
|
</Card>
|
|
434
754
|
</motion.div>
|
|
@@ -438,11 +758,17 @@ export default function TentativaPage() {
|
|
|
438
758
|
}
|
|
439
759
|
|
|
440
760
|
// Results screen
|
|
441
|
-
|
|
442
|
-
|
|
761
|
+
const result = examState?.attempt?.result;
|
|
762
|
+
if (finished && result) {
|
|
763
|
+
const passed = result.passed === true;
|
|
764
|
+
const apiAnsweredCount = examState?.attempt?.answeredCount ?? 0;
|
|
765
|
+
const totalQuestions = examState?.exam.questionCount ?? 0;
|
|
766
|
+
const questions = examState?.questions ?? [];
|
|
443
767
|
return (
|
|
444
768
|
<Page>
|
|
445
769
|
<PageHeader
|
|
770
|
+
title={examState?.exam.title ?? EXAM_TITLE}
|
|
771
|
+
description={t('breadcrumbs.examResult')}
|
|
446
772
|
breadcrumbs={[
|
|
447
773
|
{
|
|
448
774
|
label: t('breadcrumbs.home'),
|
|
@@ -463,8 +789,9 @@ export default function TentativaPage() {
|
|
|
463
789
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
464
790
|
animate={{ opacity: 1, scale: 1 }}
|
|
465
791
|
transition={{ duration: 0.5 }}
|
|
792
|
+
className="w-full max-w-4xl"
|
|
466
793
|
>
|
|
467
|
-
<Card className="
|
|
794
|
+
<Card className="border-border/70">
|
|
468
795
|
<CardHeader className="text-center">
|
|
469
796
|
<motion.div
|
|
470
797
|
initial={{ scale: 0 }}
|
|
@@ -479,13 +806,17 @@ export default function TentativaPage() {
|
|
|
479
806
|
)}
|
|
480
807
|
</motion.div>
|
|
481
808
|
<CardTitle className="text-2xl">
|
|
482
|
-
{
|
|
809
|
+
{result.hasPendingReview
|
|
810
|
+
? t('resultScreen.pendingReview')
|
|
811
|
+
: passed
|
|
812
|
+
? t('resultScreen.passed')
|
|
813
|
+
: t('resultScreen.failed')}
|
|
483
814
|
</CardTitle>
|
|
484
815
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
485
|
-
{EXAM_TITLE}
|
|
816
|
+
{examState?.exam.title ?? EXAM_TITLE}
|
|
486
817
|
</p>
|
|
487
818
|
</CardHeader>
|
|
488
|
-
<CardContent className="flex flex-col gap-
|
|
819
|
+
<CardContent className="flex flex-col gap-5">
|
|
489
820
|
<div className="text-center">
|
|
490
821
|
<motion.p
|
|
491
822
|
initial={{ opacity: 0 }}
|
|
@@ -493,63 +824,78 @@ export default function TentativaPage() {
|
|
|
493
824
|
transition={{ delay: 0.4 }}
|
|
494
825
|
className="text-5xl font-bold"
|
|
495
826
|
>
|
|
496
|
-
{
|
|
827
|
+
{result.hasPendingReview
|
|
828
|
+
? t('resultScreen.pendingBadge')
|
|
829
|
+
: `${result.percent}%`}
|
|
497
830
|
</motion.p>
|
|
498
831
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
499
|
-
{
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
832
|
+
{result.hasPendingReview
|
|
833
|
+
? t('resultScreen.pendingDescription', {
|
|
834
|
+
count: result.pendingReviewCount,
|
|
835
|
+
})
|
|
836
|
+
: t('resultScreen.scoreText', {
|
|
837
|
+
total: result.totalPoints,
|
|
838
|
+
max: result.maxPoints,
|
|
839
|
+
})}
|
|
503
840
|
</p>
|
|
504
841
|
</div>
|
|
505
842
|
|
|
506
|
-
<
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
843
|
+
<KpiCardsGrid
|
|
844
|
+
items={[
|
|
845
|
+
{
|
|
846
|
+
key: 'answered',
|
|
847
|
+
title: t('resultScreen.answered'),
|
|
848
|
+
value: apiAnsweredCount,
|
|
849
|
+
icon: CheckCircle2,
|
|
850
|
+
layout: 'compact',
|
|
851
|
+
accentClassName:
|
|
852
|
+
'from-emerald-500/25 via-green-500/15 to-transparent',
|
|
853
|
+
iconContainerClassName: 'bg-emerald-100 text-emerald-700',
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
key: 'blank',
|
|
857
|
+
title: t('resultScreen.blank'),
|
|
858
|
+
value: totalQuestions - apiAnsweredCount,
|
|
859
|
+
icon: X,
|
|
860
|
+
layout: 'compact',
|
|
861
|
+
accentClassName:
|
|
862
|
+
'from-slate-500/25 via-zinc-500/15 to-transparent',
|
|
863
|
+
iconContainerClassName: 'bg-slate-100 text-slate-700',
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
key: 'time-used',
|
|
867
|
+
title: t('resultScreen.timeUsed'),
|
|
868
|
+
value: formatTime(EXAM_TIME_MINUTES * 60 - timeLeft),
|
|
869
|
+
icon: Clock,
|
|
870
|
+
layout: 'compact',
|
|
871
|
+
accentClassName:
|
|
872
|
+
'from-sky-500/25 via-blue-500/15 to-transparent',
|
|
873
|
+
iconContainerClassName: 'bg-sky-100 text-sky-700',
|
|
874
|
+
},
|
|
875
|
+
]}
|
|
876
|
+
columns={3}
|
|
877
|
+
/>
|
|
530
878
|
|
|
531
879
|
{/* Review answers */}
|
|
532
|
-
<div className="h-auto rounded-
|
|
880
|
+
<div className="h-auto rounded-xl border border-border/70 p-4">
|
|
533
881
|
<p className="mb-2 text-sm font-medium">
|
|
534
882
|
{t('resultScreen.answersSummary')}
|
|
535
883
|
</p>
|
|
536
884
|
<div className="flex flex-col gap-2">
|
|
537
|
-
{
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
(a) => a.id === selectedAltId
|
|
885
|
+
{result.answersSummary.map((summary, i) => {
|
|
886
|
+
const q = questions.find(
|
|
887
|
+
(q) => q.id === summary.questionId
|
|
541
888
|
);
|
|
542
|
-
const isCorrect = selectedAlt?.correta;
|
|
543
889
|
return (
|
|
544
890
|
<div
|
|
545
|
-
key={
|
|
891
|
+
key={summary.questionId}
|
|
546
892
|
className="flex items-center gap-2 text-sm"
|
|
547
893
|
>
|
|
548
894
|
<span
|
|
549
895
|
className={`flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium ${
|
|
550
|
-
!
|
|
896
|
+
!summary.hasAnswer
|
|
551
897
|
? 'bg-muted text-muted-foreground'
|
|
552
|
-
: isCorrect
|
|
898
|
+
: summary.isCorrect
|
|
553
899
|
? 'bg-primary text-primary-foreground'
|
|
554
900
|
: 'bg-destructive/10 text-destructive'
|
|
555
901
|
}`}
|
|
@@ -557,14 +903,18 @@ export default function TentativaPage() {
|
|
|
557
903
|
{i + 1}
|
|
558
904
|
</span>
|
|
559
905
|
<span className="flex-1 truncate text-muted-foreground">
|
|
560
|
-
{q
|
|
906
|
+
{q
|
|
907
|
+
? `${q.statement.substring(0, 50)}...`
|
|
908
|
+
: `#${summary.questionId}`}
|
|
561
909
|
</span>
|
|
562
910
|
<span className="shrink-0 text-xs font-medium">
|
|
563
|
-
{!
|
|
911
|
+
{!summary.hasAnswer
|
|
564
912
|
? t('resultScreen.noAnswer')
|
|
565
|
-
:
|
|
566
|
-
?
|
|
567
|
-
:
|
|
913
|
+
: summary.requiresManualReview
|
|
914
|
+
? t('resultScreen.pendingAnswer')
|
|
915
|
+
: summary.isCorrect
|
|
916
|
+
? `+${summary.pointsAwarded}`
|
|
917
|
+
: '0'}
|
|
568
918
|
</span>
|
|
569
919
|
</div>
|
|
570
920
|
);
|
|
@@ -591,6 +941,11 @@ export default function TentativaPage() {
|
|
|
591
941
|
return (
|
|
592
942
|
<Page>
|
|
593
943
|
<PageHeader
|
|
944
|
+
title={examState?.exam.title ?? EXAM_TITLE}
|
|
945
|
+
description={t('examInProgress.answeredCount', {
|
|
946
|
+
count: answeredCount,
|
|
947
|
+
total: apiQuestions.length,
|
|
948
|
+
})}
|
|
594
949
|
breadcrumbs={[
|
|
595
950
|
{
|
|
596
951
|
label: t('breadcrumbs.home'),
|
|
@@ -643,38 +998,45 @@ export default function TentativaPage() {
|
|
|
643
998
|
}
|
|
644
999
|
/>
|
|
645
1000
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
</div>
|
|
650
|
-
|
|
651
|
-
{/* Top bar */}
|
|
652
|
-
<header className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur">
|
|
653
|
-
<div className="flex h-14 items-center justify-between px-4">
|
|
654
|
-
<div className="flex items-center gap-3">
|
|
1001
|
+
<Card className="mb-6 border-border/70">
|
|
1002
|
+
<CardContent className="space-y-4 p-4">
|
|
1003
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
655
1004
|
<div>
|
|
656
|
-
<p className="text-sm font-medium
|
|
657
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
1005
|
+
<p className="text-sm font-medium">
|
|
658
1006
|
{t('examInProgress.question')} {currentIndex + 1}{' '}
|
|
659
|
-
{t('examInProgress.of')} {
|
|
1007
|
+
{t('examInProgress.of')} {apiQuestions.length}
|
|
1008
|
+
</p>
|
|
1009
|
+
<p className="text-xs text-muted-foreground">
|
|
1010
|
+
{currentQuestion?.questionType
|
|
1011
|
+
? t(`examInProgress.types.${currentQuestion.questionType}`)
|
|
1012
|
+
: ''}
|
|
660
1013
|
</p>
|
|
661
1014
|
</div>
|
|
1015
|
+
<div className="flex items-center gap-2">
|
|
1016
|
+
<Badge variant="outline" className="font-mono">
|
|
1017
|
+
{formatTime(timeLeft)}
|
|
1018
|
+
</Badge>
|
|
1019
|
+
<Badge variant="secondary">
|
|
1020
|
+
{answeredCount}/{apiQuestions.length}
|
|
1021
|
+
</Badge>
|
|
1022
|
+
</div>
|
|
662
1023
|
</div>
|
|
663
|
-
|
|
664
|
-
|
|
1024
|
+
<Progress value={progressPercent} className="h-1.5" />
|
|
1025
|
+
</CardContent>
|
|
1026
|
+
</Card>
|
|
665
1027
|
|
|
666
1028
|
<div className="flex gap-6">
|
|
667
1029
|
{/* Question area */}
|
|
668
1030
|
<div className="flex-1">
|
|
669
1031
|
<AnimatePresence mode="wait">
|
|
670
1032
|
<motion.div
|
|
671
|
-
key={currentQuestion
|
|
1033
|
+
key={currentQuestion?.id ?? currentIndex}
|
|
672
1034
|
initial={{ opacity: 0, x: 20 }}
|
|
673
1035
|
animate={{ opacity: 1, x: 0 }}
|
|
674
1036
|
exit={{ opacity: 0, x: -20 }}
|
|
675
1037
|
transition={{ duration: 0.25 }}
|
|
676
1038
|
>
|
|
677
|
-
<Card>
|
|
1039
|
+
<Card className="border-border/70">
|
|
678
1040
|
<CardContent className="p-6">
|
|
679
1041
|
{/* Question header */}
|
|
680
1042
|
<div className="mb-6 flex items-start justify-between gap-4">
|
|
@@ -682,16 +1044,23 @@ export default function TentativaPage() {
|
|
|
682
1044
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary text-base font-bold text-primary-foreground">
|
|
683
1045
|
{currentIndex + 1}
|
|
684
1046
|
</div>
|
|
685
|
-
<div>
|
|
1047
|
+
<div className="space-y-2">
|
|
686
1048
|
<p className="text-base font-medium leading-relaxed">
|
|
687
|
-
{currentQuestion
|
|
1049
|
+
{currentQuestion?.statement}
|
|
688
1050
|
</p>
|
|
689
1051
|
<div className="mt-2 flex items-center gap-2">
|
|
690
1052
|
<Badge variant="outline" className="text-xs">
|
|
691
|
-
{currentQuestion
|
|
692
|
-
{currentQuestion
|
|
1053
|
+
{currentQuestion?.points} pt
|
|
1054
|
+
{currentQuestion?.points !== 1 ? 's' : ''}
|
|
693
1055
|
</Badge>
|
|
694
|
-
{
|
|
1056
|
+
{currentQuestion?.questionType && (
|
|
1057
|
+
<Badge variant="secondary" className="text-xs">
|
|
1058
|
+
{t(
|
|
1059
|
+
`examInProgress.types.${currentQuestion.questionType}`
|
|
1060
|
+
)}
|
|
1061
|
+
</Badge>
|
|
1062
|
+
)}
|
|
1063
|
+
{flagged.has(String(currentQuestion?.id)) && (
|
|
695
1064
|
<Badge variant="secondary" className="text-xs">
|
|
696
1065
|
{t('examInProgress.marked')}
|
|
697
1066
|
</Badge>
|
|
@@ -702,51 +1071,128 @@ export default function TentativaPage() {
|
|
|
702
1071
|
<Button
|
|
703
1072
|
variant="ghost"
|
|
704
1073
|
size="icon"
|
|
705
|
-
className={`shrink-0 ${flagged.has(currentQuestion
|
|
706
|
-
onClick={() => toggleFlag(currentQuestion
|
|
1074
|
+
className={`shrink-0 ${flagged.has(String(currentQuestion?.id)) ? 'text-primary' : 'text-muted-foreground'}`}
|
|
1075
|
+
onClick={() => toggleFlag(String(currentQuestion?.id))}
|
|
707
1076
|
aria-label={t('examInProgress.markQuestion')}
|
|
708
1077
|
>
|
|
709
1078
|
<Flag
|
|
710
|
-
className={`size-5 ${flagged.has(currentQuestion
|
|
1079
|
+
className={`size-5 ${flagged.has(String(currentQuestion?.id)) ? 'fill-current' : ''}`}
|
|
711
1080
|
/>
|
|
712
1081
|
</Button>
|
|
713
1082
|
</div>
|
|
714
1083
|
|
|
715
1084
|
{/* Alternatives */}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
className={`flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium transition-colors ${
|
|
1085
|
+
{isObjectiveQuestion(currentQuestion?.questionType) ? (
|
|
1086
|
+
<div className="flex flex-col gap-3">
|
|
1087
|
+
{(currentQuestion?.alternatives ?? []).map((alt, i) => {
|
|
1088
|
+
const isSelected =
|
|
1089
|
+
answers[String(currentQuestion?.id)]?.examOptionId ===
|
|
1090
|
+
alt.id;
|
|
1091
|
+
return (
|
|
1092
|
+
<motion.button
|
|
1093
|
+
key={alt.id}
|
|
1094
|
+
whileHover={{ scale: 1.01 }}
|
|
1095
|
+
whileTap={{ scale: 0.99 }}
|
|
1096
|
+
onClick={() =>
|
|
1097
|
+
selectAnswer(
|
|
1098
|
+
String(currentQuestion?.id),
|
|
1099
|
+
String(alt.id)
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
className={`flex items-center gap-3 rounded-xl border p-4 text-left text-sm transition-all ${
|
|
735
1103
|
isSelected
|
|
736
|
-
? 'border-primary bg-primary
|
|
737
|
-
: 'border-border'
|
|
1104
|
+
? 'border-primary bg-primary/5 font-medium'
|
|
1105
|
+
: 'border-border hover:border-primary/30 hover:bg-muted/50'
|
|
738
1106
|
}`}
|
|
739
1107
|
>
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1108
|
+
<span
|
|
1109
|
+
className={`flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-medium transition-colors ${
|
|
1110
|
+
isSelected
|
|
1111
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
1112
|
+
: 'border-border'
|
|
1113
|
+
}`}
|
|
1114
|
+
>
|
|
1115
|
+
{String.fromCharCode(65 + i)}
|
|
1116
|
+
</span>
|
|
1117
|
+
<span className="flex-1">{alt.text}</span>
|
|
1118
|
+
{isSelected && (
|
|
1119
|
+
<CircleDot className="size-5 shrink-0" />
|
|
1120
|
+
)}
|
|
1121
|
+
</motion.button>
|
|
1122
|
+
);
|
|
1123
|
+
})}
|
|
1124
|
+
</div>
|
|
1125
|
+
) : currentQuestion?.questionType === 'matching' ? (
|
|
1126
|
+
<div className="space-y-3">
|
|
1127
|
+
<p className="text-sm text-muted-foreground">
|
|
1128
|
+
{t('examInProgress.matchingDescription')}
|
|
1129
|
+
</p>
|
|
1130
|
+
{(currentQuestion?.matchingPairs ?? []).map((pair) => {
|
|
1131
|
+
const selectedRightId = (
|
|
1132
|
+
answers[String(currentQuestion?.id)]?.matchingPairs ??
|
|
1133
|
+
[]
|
|
1134
|
+
).find((item) => item.leftId === pair.id)?.rightId;
|
|
1135
|
+
|
|
1136
|
+
return (
|
|
1137
|
+
<div
|
|
1138
|
+
key={pair.id}
|
|
1139
|
+
className="grid gap-2 rounded-lg border p-3 md:grid-cols-2"
|
|
1140
|
+
>
|
|
1141
|
+
<p className="text-sm font-medium">
|
|
1142
|
+
{pair.leftText}
|
|
1143
|
+
</p>
|
|
1144
|
+
<select
|
|
1145
|
+
value={selectedRightId ?? ''}
|
|
1146
|
+
onChange={(event) =>
|
|
1147
|
+
updateMatchingAnswer(
|
|
1148
|
+
String(currentQuestion?.id),
|
|
1149
|
+
pair.id,
|
|
1150
|
+
event.target.value
|
|
1151
|
+
)
|
|
1152
|
+
}
|
|
1153
|
+
className="h-9 rounded-md border bg-background px-3 text-sm"
|
|
1154
|
+
>
|
|
1155
|
+
<option value="">
|
|
1156
|
+
{t('examInProgress.matchingSelectPlaceholder')}
|
|
1157
|
+
</option>
|
|
1158
|
+
{(currentQuestion?.matchingOptions ?? []).map(
|
|
1159
|
+
(option) => (
|
|
1160
|
+
<option key={option.id} value={option.id}>
|
|
1161
|
+
{option.text}
|
|
1162
|
+
</option>
|
|
1163
|
+
)
|
|
1164
|
+
)}
|
|
1165
|
+
</select>
|
|
1166
|
+
</div>
|
|
1167
|
+
);
|
|
1168
|
+
})}
|
|
1169
|
+
</div>
|
|
1170
|
+
) : (
|
|
1171
|
+
<div className="space-y-3">
|
|
1172
|
+
<p className="text-sm text-muted-foreground">
|
|
1173
|
+
{currentQuestion?.questionType === 'fill_blank'
|
|
1174
|
+
? t('examInProgress.fillBlankDescription')
|
|
1175
|
+
: t('examInProgress.essayDescription')}
|
|
1176
|
+
</p>
|
|
1177
|
+
<Textarea
|
|
1178
|
+
rows={10}
|
|
1179
|
+
value={
|
|
1180
|
+
answers[String(currentQuestion?.id)]?.answerText ?? ''
|
|
1181
|
+
}
|
|
1182
|
+
onChange={(event) =>
|
|
1183
|
+
updateTextAnswer(
|
|
1184
|
+
String(currentQuestion?.id),
|
|
1185
|
+
event.target.value
|
|
1186
|
+
)
|
|
1187
|
+
}
|
|
1188
|
+
placeholder={
|
|
1189
|
+
currentQuestion?.questionType === 'fill_blank'
|
|
1190
|
+
? t('examInProgress.fillBlankPlaceholder')
|
|
1191
|
+
: t('examInProgress.essayPlaceholder')
|
|
1192
|
+
}
|
|
1193
|
+
/>
|
|
1194
|
+
</div>
|
|
1195
|
+
)}
|
|
750
1196
|
|
|
751
1197
|
{/* Navigation */}
|
|
752
1198
|
<div className="mt-8 flex items-center justify-between">
|
|
@@ -761,12 +1207,12 @@ export default function TentativaPage() {
|
|
|
761
1207
|
</Button>
|
|
762
1208
|
<Button
|
|
763
1209
|
variant="ghost"
|
|
764
|
-
className=
|
|
1210
|
+
className="lg:hidden"
|
|
765
1211
|
onClick={() => setShowNav(!showNav)}
|
|
766
1212
|
>
|
|
767
|
-
{currentIndex + 1}/{
|
|
1213
|
+
{currentIndex + 1}/{apiQuestions.length}
|
|
768
1214
|
</Button>
|
|
769
|
-
{currentIndex <
|
|
1215
|
+
{currentIndex < apiQuestions.length - 1 ? (
|
|
770
1216
|
<Button
|
|
771
1217
|
className="gap-2"
|
|
772
1218
|
onClick={() => setCurrentIndex((p) => p + 1)}
|
|
@@ -792,7 +1238,7 @@ export default function TentativaPage() {
|
|
|
792
1238
|
|
|
793
1239
|
{/* Side navigation - desktop */}
|
|
794
1240
|
<div className="hidden w-64 shrink-0 lg:block">
|
|
795
|
-
<Card className="sticky top-20">
|
|
1241
|
+
<Card className="sticky top-20 border-border/70">
|
|
796
1242
|
<CardHeader className="pb-3">
|
|
797
1243
|
<CardTitle className="text-sm font-medium">
|
|
798
1244
|
{t('examInProgress.navigation')}
|
|
@@ -800,15 +1246,15 @@ export default function TentativaPage() {
|
|
|
800
1246
|
<p className="text-xs text-muted-foreground">
|
|
801
1247
|
{t('examInProgress.answeredCount', {
|
|
802
1248
|
count: answeredCount,
|
|
803
|
-
total:
|
|
1249
|
+
total: apiQuestions.length,
|
|
804
1250
|
})}
|
|
805
1251
|
</p>
|
|
806
1252
|
</CardHeader>
|
|
807
1253
|
<CardContent className="pb-4">
|
|
808
1254
|
<div className="grid grid-cols-5 gap-2">
|
|
809
|
-
{
|
|
810
|
-
const isAnswered =
|
|
811
|
-
const isFlagged = flagged.has(q.id);
|
|
1255
|
+
{apiQuestions.map((q, i) => {
|
|
1256
|
+
const isAnswered = hasAnswerValue(answers[String(q.id)]);
|
|
1257
|
+
const isFlagged = flagged.has(String(q.id));
|
|
812
1258
|
const isCurrent = i === currentIndex;
|
|
813
1259
|
return (
|
|
814
1260
|
<button
|
|
@@ -819,7 +1265,7 @@ export default function TentativaPage() {
|
|
|
819
1265
|
? 'bg-primary text-primary-foreground ring-1 ring-primary ring-offset-2'
|
|
820
1266
|
: isAnswered
|
|
821
1267
|
? 'bg-primary/10 text-primary'
|
|
822
|
-
: 'bg-muted text-muted-foreground hover:bg-muted
|
|
1268
|
+
: 'bg-muted/80 text-muted-foreground hover:bg-muted'
|
|
823
1269
|
}`}
|
|
824
1270
|
aria-label={`${t('examInProgress.questionLabel', { number: i + 1 })}${isAnswered ? ` ${t('examInProgress.questionAnswered')}` : ''}${isFlagged ? ` ${t('examInProgress.questionMarked')}` : ''}`}
|
|
825
1271
|
>
|
|
@@ -885,13 +1331,13 @@ export default function TentativaPage() {
|
|
|
885
1331
|
<p className="mb-3 text-sm font-medium">
|
|
886
1332
|
{t('examInProgress.navigationMobile', {
|
|
887
1333
|
count: answeredCount,
|
|
888
|
-
total:
|
|
1334
|
+
total: apiQuestions.length,
|
|
889
1335
|
})}
|
|
890
1336
|
</p>
|
|
891
1337
|
<div className="grid grid-cols-8 gap-2">
|
|
892
|
-
{
|
|
893
|
-
const isAnswered =
|
|
894
|
-
const isFlagged = flagged.has(q.id);
|
|
1338
|
+
{apiQuestions.map((q, i) => {
|
|
1339
|
+
const isAnswered = hasAnswerValue(answers[String(q.id)]);
|
|
1340
|
+
const isFlagged = flagged.has(String(q.id));
|
|
895
1341
|
const isCurrent = i === currentIndex;
|
|
896
1342
|
return (
|
|
897
1343
|
<button
|
|
@@ -932,16 +1378,16 @@ export default function TentativaPage() {
|
|
|
932
1378
|
<DialogDescription asChild>
|
|
933
1379
|
<div>
|
|
934
1380
|
<p>{t('submitDialog.description')}</p>
|
|
935
|
-
<div className="mt-3
|
|
1381
|
+
<div className="mt-3 grid gap-3 rounded-xl bg-muted p-4 text-sm md:grid-cols-3">
|
|
936
1382
|
<div>
|
|
937
1383
|
<p className="font-medium text-primary">
|
|
938
|
-
{answeredCount}/{
|
|
1384
|
+
{answeredCount}/{apiQuestions.length}
|
|
939
1385
|
</p>
|
|
940
1386
|
<p className="text-xs">{t('submitDialog.answeredLabel')}</p>
|
|
941
1387
|
</div>
|
|
942
1388
|
<div>
|
|
943
1389
|
<p className="font-medium text-primary">
|
|
944
|
-
{
|
|
1390
|
+
{apiQuestions.length - answeredCount}
|
|
945
1391
|
</p>
|
|
946
1392
|
<p className="text-xs">{t('submitDialog.blankLabel')}</p>
|
|
947
1393
|
</div>
|
|
@@ -950,10 +1396,10 @@ export default function TentativaPage() {
|
|
|
950
1396
|
<p className="text-xs">{t('submitDialog.markedLabel')}</p>
|
|
951
1397
|
</div>
|
|
952
1398
|
</div>
|
|
953
|
-
{
|
|
1399
|
+
{apiQuestions.length - answeredCount > 0 && (
|
|
954
1400
|
<p className="mt-2 text-sm text-destructive">
|
|
955
1401
|
{t('submitDialog.warning', {
|
|
956
|
-
count:
|
|
1402
|
+
count: apiQuestions.length - answeredCount,
|
|
957
1403
|
})}
|
|
958
1404
|
</p>
|
|
959
1405
|
)}
|
|
@@ -967,7 +1413,7 @@ export default function TentativaPage() {
|
|
|
967
1413
|
>
|
|
968
1414
|
{t('submitDialog.continueButton')}
|
|
969
1415
|
</Button>
|
|
970
|
-
<Button onClick={handleSubmit}>
|
|
1416
|
+
<Button onClick={() => handleSubmit()}>
|
|
971
1417
|
{t('submitDialog.submitButton')}
|
|
972
1418
|
</Button>
|
|
973
1419
|
</DialogFooter>
|