@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,11 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { LmsClassCalendar } from '@/app/(app)/(libraries)/lms/_components/lms-class-calendar';
|
|
4
|
+
import { CopyButton } from '@/components/copy-button';
|
|
5
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
6
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
5
7
|
import { Badge } from '@/components/ui/badge';
|
|
6
8
|
import { Button } from '@/components/ui/button';
|
|
7
9
|
import { Card, CardContent } from '@/components/ui/card';
|
|
8
10
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
11
|
+
import {
|
|
12
|
+
Command,
|
|
13
|
+
CommandEmpty,
|
|
14
|
+
CommandGroup,
|
|
15
|
+
CommandInput,
|
|
16
|
+
CommandItem,
|
|
17
|
+
CommandList,
|
|
18
|
+
} from '@/components/ui/command';
|
|
9
19
|
import {
|
|
10
20
|
Dialog,
|
|
11
21
|
DialogContent,
|
|
@@ -22,7 +32,21 @@ import {
|
|
|
22
32
|
DropdownMenuTrigger,
|
|
23
33
|
} from '@/components/ui/dropdown-menu';
|
|
24
34
|
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
|
35
|
+
import {
|
|
36
|
+
Form,
|
|
37
|
+
FormControl,
|
|
38
|
+
FormField,
|
|
39
|
+
FormItem,
|
|
40
|
+
FormLabel,
|
|
41
|
+
FormMessage,
|
|
42
|
+
} from '@/components/ui/form';
|
|
25
43
|
import { Input } from '@/components/ui/input';
|
|
44
|
+
import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
|
|
45
|
+
import {
|
|
46
|
+
Popover,
|
|
47
|
+
PopoverContent,
|
|
48
|
+
PopoverTrigger,
|
|
49
|
+
} from '@/components/ui/popover';
|
|
26
50
|
import {
|
|
27
51
|
Select,
|
|
28
52
|
SelectContent,
|
|
@@ -40,9 +64,31 @@ import {
|
|
|
40
64
|
} from '@/components/ui/sheet';
|
|
41
65
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
42
66
|
import { Switch } from '@/components/ui/switch';
|
|
67
|
+
import {
|
|
68
|
+
Table,
|
|
69
|
+
TableBody,
|
|
70
|
+
TableCell,
|
|
71
|
+
TableHead,
|
|
72
|
+
TableHeader,
|
|
73
|
+
TableRow,
|
|
74
|
+
} from '@/components/ui/table';
|
|
43
75
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
76
|
+
import { cn } from '@/lib/utils';
|
|
77
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
44
78
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
45
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
addMonths,
|
|
81
|
+
eachDayOfInterval,
|
|
82
|
+
endOfMonth,
|
|
83
|
+
endOfWeek,
|
|
84
|
+
format,
|
|
85
|
+
isSameDay,
|
|
86
|
+
isSameMonth,
|
|
87
|
+
setHours,
|
|
88
|
+
setMinutes,
|
|
89
|
+
startOfMonth,
|
|
90
|
+
startOfWeek,
|
|
91
|
+
} from 'date-fns';
|
|
46
92
|
import { enUS, ptBR } from 'date-fns/locale';
|
|
47
93
|
import { motion } from 'framer-motion';
|
|
48
94
|
import {
|
|
@@ -51,13 +97,16 @@ import {
|
|
|
51
97
|
Calendar as CalendarIcon,
|
|
52
98
|
Check,
|
|
53
99
|
CheckCircle2,
|
|
100
|
+
ChevronLeft,
|
|
101
|
+
ChevronRight,
|
|
102
|
+
ChevronsUpDown,
|
|
54
103
|
Clock,
|
|
55
104
|
Eye,
|
|
56
105
|
Loader2,
|
|
57
|
-
Mail,
|
|
58
106
|
MapPin,
|
|
59
107
|
Monitor,
|
|
60
108
|
MoreHorizontal,
|
|
109
|
+
Pencil,
|
|
61
110
|
Plus,
|
|
62
111
|
Save,
|
|
63
112
|
Search,
|
|
@@ -67,175 +116,464 @@ import {
|
|
|
67
116
|
Video,
|
|
68
117
|
} from 'lucide-react';
|
|
69
118
|
import { useLocale, useTranslations } from 'next-intl';
|
|
70
|
-
import Link from 'next/link';
|
|
71
119
|
import { useParams, useRouter } from 'next/navigation';
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
import {
|
|
121
|
+
useCallback,
|
|
122
|
+
useEffect,
|
|
123
|
+
useMemo,
|
|
124
|
+
useState,
|
|
125
|
+
type ReactNode,
|
|
126
|
+
} from 'react';
|
|
75
127
|
import { Controller, useForm } from 'react-hook-form';
|
|
76
128
|
import { toast } from 'sonner';
|
|
77
129
|
import { z } from 'zod';
|
|
130
|
+
import { ClassFormSheet } from '../../_components/class-form-sheet';
|
|
131
|
+
import { CreateLmsPersonSheet } from '../../_components/create-lms-person-sheet';
|
|
132
|
+
import { CreateLmsStudentPersonSheet } from '../../_components/create-lms-student-person-sheet';
|
|
78
133
|
|
|
79
134
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
80
135
|
|
|
81
136
|
interface Aluno {
|
|
82
137
|
id: number;
|
|
138
|
+
enrollmentId: number;
|
|
83
139
|
nome: string;
|
|
84
140
|
email: string;
|
|
85
141
|
telefone: string;
|
|
86
|
-
avatar?: string;
|
|
87
142
|
matriculadoEm: string;
|
|
88
143
|
progresso: number;
|
|
89
|
-
|
|
144
|
+
status: string;
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
interface Aula {
|
|
93
148
|
id: number;
|
|
94
149
|
titulo: string;
|
|
95
|
-
data:
|
|
150
|
+
data: string; // ISO date string from API
|
|
96
151
|
horaInicio: string;
|
|
97
152
|
horaFim: string;
|
|
98
153
|
local: string;
|
|
154
|
+
meetingUrl: string;
|
|
99
155
|
tipo: 'presencial' | 'online';
|
|
156
|
+
status: string;
|
|
157
|
+
instructorId?: number | null;
|
|
158
|
+
instructorName?: string;
|
|
159
|
+
recurrence?: {
|
|
160
|
+
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
|
161
|
+
interval: number;
|
|
162
|
+
until: string;
|
|
163
|
+
daysOfWeek?: Array<'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU'>;
|
|
164
|
+
} | null;
|
|
165
|
+
recurrenceId?: string | null;
|
|
166
|
+
occurrenceIndex?: number | null;
|
|
167
|
+
isException?: boolean;
|
|
168
|
+
isRecurring?: boolean;
|
|
169
|
+
cor?: string | null;
|
|
170
|
+
color?: string | null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type SessionRecurrenceFrequency =
|
|
174
|
+
| 'none'
|
|
175
|
+
| 'daily'
|
|
176
|
+
| 'weekly'
|
|
177
|
+
| 'monthly'
|
|
178
|
+
| 'yearly';
|
|
179
|
+
|
|
180
|
+
type SessionRecurrenceDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
|
|
181
|
+
|
|
182
|
+
interface InstructorOption {
|
|
183
|
+
id: number;
|
|
184
|
+
name: string;
|
|
185
|
+
personId?: number;
|
|
186
|
+
qualificationSlugs?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type InstructorApiRow = {
|
|
190
|
+
id?: number | string;
|
|
191
|
+
instructor_id?: number | string;
|
|
192
|
+
value?: number | string;
|
|
193
|
+
name?: string;
|
|
194
|
+
nome?: string;
|
|
195
|
+
full_name?: string;
|
|
196
|
+
label?: string;
|
|
197
|
+
personId?: number | string;
|
|
198
|
+
person_id?: number | string;
|
|
199
|
+
qualificationSlugs?: string[];
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
interface Person {
|
|
203
|
+
id: number;
|
|
204
|
+
nome: string;
|
|
205
|
+
email: string;
|
|
206
|
+
isInstructor?: boolean;
|
|
207
|
+
canTeachCourses?: boolean;
|
|
208
|
+
isStudentByEnrollment?: boolean;
|
|
100
209
|
}
|
|
101
210
|
|
|
102
211
|
interface PresencaItem {
|
|
103
212
|
alunoId: number;
|
|
213
|
+
selecionado: boolean;
|
|
104
214
|
presente: boolean;
|
|
105
215
|
}
|
|
106
216
|
|
|
107
|
-
|
|
217
|
+
interface OpenAulaSheetOptions {
|
|
218
|
+
initialTab?: 'aulas' | 'chamada';
|
|
219
|
+
prefill?: Partial<AulaForm>;
|
|
220
|
+
attendance?: PresencaItem[];
|
|
221
|
+
}
|
|
108
222
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
223
|
+
interface StudentProfile {
|
|
224
|
+
id: number;
|
|
225
|
+
enrollmentId: number;
|
|
226
|
+
nome: string;
|
|
227
|
+
email: string;
|
|
228
|
+
telefone: string;
|
|
229
|
+
matriculadoEm: string;
|
|
230
|
+
progresso: number;
|
|
231
|
+
status: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getErrorMessage(error: unknown) {
|
|
235
|
+
if (error instanceof Error && error.message) {
|
|
236
|
+
return error.message;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof error === 'object' && error && 'message' in error) {
|
|
240
|
+
const message = (error as { message?: unknown }).message;
|
|
241
|
+
if (typeof message === 'string' && message.trim()) {
|
|
242
|
+
return message;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getPersonInitials(name: string) {
|
|
250
|
+
return name
|
|
251
|
+
.split(' ')
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
254
|
+
.slice(0, 2)
|
|
255
|
+
.join('');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getSessionStartDate(aula: Aula) {
|
|
259
|
+
const date = parseSessionDate(aula.data);
|
|
260
|
+
const [hour = 0, minute = 0] = aula.horaInicio.split(':').map(Number);
|
|
261
|
+
|
|
262
|
+
return setMinutes(setHours(date, hour), minute);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getSessionEndDate(aula: Aula) {
|
|
266
|
+
const date = parseSessionDate(aula.data);
|
|
267
|
+
const [hour = 0, minute = 0] = aula.horaFim.split(':').map(Number);
|
|
268
|
+
|
|
269
|
+
return setMinutes(setHours(date, hour), minute);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function DetailMetaItem({
|
|
273
|
+
icon,
|
|
274
|
+
label,
|
|
275
|
+
value,
|
|
276
|
+
action,
|
|
277
|
+
}: {
|
|
278
|
+
icon: ReactNode;
|
|
279
|
+
label: string;
|
|
280
|
+
value: ReactNode;
|
|
281
|
+
action?: ReactNode;
|
|
282
|
+
}) {
|
|
283
|
+
return (
|
|
284
|
+
<div className="rounded-xl border border-border/70 bg-background/70 px-3 py-2.5">
|
|
285
|
+
<div className="mb-1.5 flex items-center gap-2">
|
|
286
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border border-border/70 bg-muted/50 text-muted-foreground">
|
|
287
|
+
{icon}
|
|
288
|
+
</div>
|
|
289
|
+
<span className="text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
290
|
+
{label}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="flex items-center justify-between gap-3">
|
|
294
|
+
<div className="min-w-0 text-sm font-medium text-foreground">
|
|
295
|
+
{value}
|
|
296
|
+
</div>
|
|
297
|
+
{action}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function OperationalSidebarCard({
|
|
304
|
+
title,
|
|
305
|
+
children,
|
|
306
|
+
className,
|
|
307
|
+
}: {
|
|
308
|
+
title: string;
|
|
309
|
+
children: ReactNode;
|
|
310
|
+
className?: string;
|
|
311
|
+
}) {
|
|
312
|
+
return (
|
|
313
|
+
<Card className={cn('overflow-hidden border-border/70 py-0', className)}>
|
|
314
|
+
<CardContent className="space-y-3 p-4">
|
|
315
|
+
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
316
|
+
{title}
|
|
317
|
+
</div>
|
|
318
|
+
{children}
|
|
319
|
+
</CardContent>
|
|
320
|
+
</Card>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type ClassDetail = {
|
|
325
|
+
id: number;
|
|
326
|
+
code?: string;
|
|
327
|
+
codigo?: string;
|
|
328
|
+
courseId?: number;
|
|
329
|
+
cursoId?: number;
|
|
330
|
+
courseTitle?: string;
|
|
331
|
+
curso?: string;
|
|
332
|
+
status?: string;
|
|
333
|
+
capacity?: number;
|
|
334
|
+
startDate?: string;
|
|
335
|
+
dataInicio?: string;
|
|
336
|
+
endDate?: string;
|
|
337
|
+
dataFim?: string;
|
|
338
|
+
startTime?: string;
|
|
339
|
+
endTime?: string;
|
|
340
|
+
horario?: string;
|
|
341
|
+
deliveryMode?: 'presential' | 'online' | 'hybrid';
|
|
342
|
+
tipo?: 'presencial' | 'online' | 'hibrida';
|
|
343
|
+
virtualRoomUrl?: string;
|
|
344
|
+
local?: string;
|
|
345
|
+
location?: string;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
type AttendanceRecord = {
|
|
349
|
+
student_id: number;
|
|
350
|
+
present: boolean;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
type Locale = {
|
|
354
|
+
id?: number;
|
|
355
|
+
code: string;
|
|
356
|
+
name: string;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const EMPTY_AULAS: Aula[] = [];
|
|
360
|
+
|
|
361
|
+
const SESSION_RECURRENCE_FREQUENCIES = [
|
|
362
|
+
'none',
|
|
363
|
+
'daily',
|
|
364
|
+
'weekly',
|
|
365
|
+
'monthly',
|
|
366
|
+
'yearly',
|
|
367
|
+
] as const;
|
|
368
|
+
|
|
369
|
+
const SESSION_RECURRENCE_DAYS = [
|
|
370
|
+
'MO',
|
|
371
|
+
'TU',
|
|
372
|
+
'WE',
|
|
373
|
+
'TH',
|
|
374
|
+
'FR',
|
|
375
|
+
'SA',
|
|
376
|
+
'SU',
|
|
377
|
+
] as const;
|
|
378
|
+
|
|
379
|
+
const SESSION_COLOR_PALETTE = [
|
|
380
|
+
'#3b82f6',
|
|
381
|
+
'#22c55e',
|
|
382
|
+
'#f59e0b',
|
|
383
|
+
'#ef4444',
|
|
384
|
+
'#8b5cf6',
|
|
385
|
+
'#06b6d4',
|
|
386
|
+
'#f97316',
|
|
387
|
+
'#64748b',
|
|
388
|
+
] as const;
|
|
389
|
+
|
|
390
|
+
const SESSION_DEFAULT_COLOR = SESSION_COLOR_PALETTE[0];
|
|
391
|
+
|
|
392
|
+
const getAulaDisplayColor = (aula?: {
|
|
393
|
+
cor?: string | null;
|
|
394
|
+
color?: string | null;
|
|
395
|
+
tipo?: 'presencial' | 'online';
|
|
396
|
+
}) => {
|
|
397
|
+
if (aula?.cor) return aula.cor;
|
|
398
|
+
if (aula?.color) return aula.color;
|
|
399
|
+
|
|
400
|
+
return aula?.tipo === 'presencial' ? '#3b82f6' : '#22c55e';
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const RECURRENCE_DAY_LABELS: Array<{
|
|
404
|
+
value: SessionRecurrenceDay;
|
|
405
|
+
labelKey: string;
|
|
406
|
+
}> = [
|
|
407
|
+
{ value: 'MO', labelKey: 'sheet.lessonForm.recurrence.days.monday' },
|
|
408
|
+
{ value: 'TU', labelKey: 'sheet.lessonForm.recurrence.days.tuesday' },
|
|
409
|
+
{ value: 'WE', labelKey: 'sheet.lessonForm.recurrence.days.wednesday' },
|
|
410
|
+
{ value: 'TH', labelKey: 'sheet.lessonForm.recurrence.days.thursday' },
|
|
411
|
+
{ value: 'FR', labelKey: 'sheet.lessonForm.recurrence.days.friday' },
|
|
412
|
+
{ value: 'SA', labelKey: 'sheet.lessonForm.recurrence.days.saturday' },
|
|
413
|
+
{ value: 'SU', labelKey: 'sheet.lessonForm.recurrence.days.sunday' },
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const parseSessionDate = (value: string) => {
|
|
417
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value ?? '');
|
|
418
|
+
|
|
419
|
+
if (match) {
|
|
420
|
+
const year = Number(match[1]);
|
|
421
|
+
const month = Number(match[2]);
|
|
422
|
+
const day = Number(match[3]);
|
|
423
|
+
|
|
424
|
+
if (
|
|
425
|
+
Number.isFinite(year) &&
|
|
426
|
+
Number.isFinite(month) &&
|
|
427
|
+
Number.isFinite(day)
|
|
428
|
+
) {
|
|
429
|
+
return new Date(year, month - 1, day, 12, 0, 0, 0);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return new Date(value);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const toSessionInputDate = (value: string) => {
|
|
437
|
+
const date = parseSessionDate(value);
|
|
438
|
+
return format(date, 'yyyy-MM-dd');
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const getDayCodeFromDate = (value: string): SessionRecurrenceDay => {
|
|
442
|
+
const day = parseSessionDate(value).getDay();
|
|
443
|
+
return ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][
|
|
444
|
+
day
|
|
445
|
+
] as SessionRecurrenceDay;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const buildSessionRecurrence = (data: {
|
|
449
|
+
data: string;
|
|
450
|
+
recurrenceFrequency: SessionRecurrenceFrequency;
|
|
451
|
+
recurrenceUntil?: string;
|
|
452
|
+
recurrenceDaysOfWeek?: SessionRecurrenceDay[];
|
|
453
|
+
}) => {
|
|
454
|
+
if (data.recurrenceFrequency === 'none') {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
frequency: data.recurrenceFrequency,
|
|
460
|
+
interval: 1,
|
|
461
|
+
until: data.recurrenceUntil,
|
|
462
|
+
...(data.recurrenceFrequency === 'weekly'
|
|
463
|
+
? {
|
|
464
|
+
daysOfWeek:
|
|
465
|
+
data.recurrenceDaysOfWeek && data.recurrenceDaysOfWeek.length > 0
|
|
466
|
+
? data.recurrenceDaysOfWeek
|
|
467
|
+
: [getDayCodeFromDate(data.data)],
|
|
468
|
+
}
|
|
469
|
+
: {}),
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
function normalizeInstructorOption(
|
|
474
|
+
item: InstructorApiRow
|
|
475
|
+
): InstructorOption | null {
|
|
476
|
+
const id = Number(item?.id ?? item?.instructor_id ?? item?.value ?? 0);
|
|
477
|
+
const name = String(
|
|
478
|
+
item?.name ?? item?.nome ?? item?.full_name ?? item?.label ?? ''
|
|
479
|
+
).trim();
|
|
480
|
+
|
|
481
|
+
if (!id || !name) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
id,
|
|
487
|
+
name,
|
|
488
|
+
personId: Number(item?.personId ?? item?.person_id ?? 0) || undefined,
|
|
489
|
+
qualificationSlugs: Array.isArray(item?.qualificationSlugs)
|
|
490
|
+
? item.qualificationSlugs
|
|
491
|
+
: undefined,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
117
494
|
|
|
118
495
|
// ── Schemas ───────────────────────────────────────────────────────────────────
|
|
119
496
|
|
|
120
497
|
const getAulaSchema = (t: (key: string) => string) =>
|
|
498
|
+
z
|
|
499
|
+
.object({
|
|
500
|
+
titulo: z.string().min(3, t('sheet.lessonForm.validation.titleMin')),
|
|
501
|
+
data: z.string().min(1, t('sheet.lessonForm.validation.dateRequired')),
|
|
502
|
+
horaInicio: z
|
|
503
|
+
.string()
|
|
504
|
+
.min(1, t('sheet.lessonForm.validation.startTimeRequired')),
|
|
505
|
+
horaFim: z
|
|
506
|
+
.string()
|
|
507
|
+
.min(1, t('sheet.lessonForm.validation.endTimeRequired')),
|
|
508
|
+
local: z
|
|
509
|
+
.string()
|
|
510
|
+
.min(1, t('sheet.lessonForm.validation.locationRequired')),
|
|
511
|
+
tipo: z.string().min(1, t('sheet.lessonForm.validation.typeRequired')),
|
|
512
|
+
instrutorId: z.string().optional(),
|
|
513
|
+
recurrenceFrequency: z
|
|
514
|
+
.enum(SESSION_RECURRENCE_FREQUENCIES)
|
|
515
|
+
.default('none'),
|
|
516
|
+
recurrenceUntil: z.string().optional(),
|
|
517
|
+
recurrenceDaysOfWeek: z
|
|
518
|
+
.array(z.enum(SESSION_RECURRENCE_DAYS))
|
|
519
|
+
.default([]),
|
|
520
|
+
cor: z
|
|
521
|
+
.string()
|
|
522
|
+
.regex(
|
|
523
|
+
/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,
|
|
524
|
+
t('sheet.lessonForm.validation.colorInvalid')
|
|
525
|
+
)
|
|
526
|
+
.default(SESSION_DEFAULT_COLOR),
|
|
527
|
+
applyScope: z.enum(['single', 'series']).default('single'),
|
|
528
|
+
})
|
|
529
|
+
.superRefine((values, context) => {
|
|
530
|
+
if (values.recurrenceFrequency === 'none') {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!values.recurrenceUntil) {
|
|
535
|
+
context.addIssue({
|
|
536
|
+
code: z.ZodIssueCode.custom,
|
|
537
|
+
message: t('sheet.lessonForm.validation.recurrenceUntilRequired'),
|
|
538
|
+
path: ['recurrenceUntil'],
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (values.recurrenceUntil < values.data) {
|
|
544
|
+
context.addIssue({
|
|
545
|
+
code: z.ZodIssueCode.custom,
|
|
546
|
+
message: t('sheet.lessonForm.validation.recurrenceUntilAfterStart'),
|
|
547
|
+
path: ['recurrenceUntil'],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const getStudentSchema = (t: (key: string) => string) =>
|
|
121
553
|
z.object({
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
horaInicio: z
|
|
554
|
+
name: z.string().trim().min(3, t('dialogs.studentValidation.nameMin')),
|
|
555
|
+
email: z
|
|
125
556
|
.string()
|
|
126
|
-
.
|
|
127
|
-
|
|
557
|
+
.trim()
|
|
558
|
+
.max(255)
|
|
559
|
+
.refine(
|
|
560
|
+
(value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
561
|
+
t('dialogs.studentValidation.emailInvalid')
|
|
562
|
+
),
|
|
563
|
+
phone: z
|
|
128
564
|
.string()
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
565
|
+
.trim()
|
|
566
|
+
.max(50)
|
|
567
|
+
.refine(
|
|
568
|
+
(value) => !value || /^[0-9+()\s-]{8,20}$/.test(value),
|
|
569
|
+
t('dialogs.studentValidation.phoneInvalid')
|
|
570
|
+
),
|
|
132
571
|
});
|
|
133
572
|
|
|
134
573
|
type AulaForm = z.infer<ReturnType<typeof getAulaSchema>>;
|
|
574
|
+
type StudentForm = z.infer<ReturnType<typeof getStudentSchema>>;
|
|
135
575
|
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
const TURMA_MOCK = {
|
|
139
|
-
id: 1,
|
|
140
|
-
codigo: 'T2024-001',
|
|
141
|
-
curso: 'React Avancado',
|
|
142
|
-
cursoId: 1,
|
|
143
|
-
tipo: 'online' as const,
|
|
144
|
-
dataInicio: '2024-02-01',
|
|
145
|
-
dataFim: '2024-06-30',
|
|
146
|
-
horario: '19:00 - 22:00',
|
|
147
|
-
status: 'em_andamento' as const,
|
|
148
|
-
vagas: 30,
|
|
149
|
-
matriculados: 28,
|
|
150
|
-
professor: 'Prof. Marcos Silva',
|
|
151
|
-
local: 'https://meet.google.com/abc-defg-hij',
|
|
152
|
-
descricao:
|
|
153
|
-
'Turma focada em conceitos avancados de React, incluindo hooks customizados, performance e arquitetura.',
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
function generateAlunos(): Aluno[] {
|
|
157
|
-
const nomes = [
|
|
158
|
-
'Ana Silva',
|
|
159
|
-
'Bruno Costa',
|
|
160
|
-
'Carla Oliveira',
|
|
161
|
-
'Daniel Santos',
|
|
162
|
-
'Elena Ferreira',
|
|
163
|
-
'Felipe Souza',
|
|
164
|
-
'Gabriela Lima',
|
|
165
|
-
'Henrique Almeida',
|
|
166
|
-
'Isabela Rocha',
|
|
167
|
-
'João Pedro',
|
|
168
|
-
'Katia Martins',
|
|
169
|
-
'Lucas Ribeiro',
|
|
170
|
-
'Maria Clara',
|
|
171
|
-
'Nicolas Pereira',
|
|
172
|
-
'Olivia Gomes',
|
|
173
|
-
'Paulo Henrique',
|
|
174
|
-
'Raquel Dias',
|
|
175
|
-
'Samuel Nunes',
|
|
176
|
-
'Tatiana Vieira',
|
|
177
|
-
'Vinicius Castro',
|
|
178
|
-
'William Araújo',
|
|
179
|
-
'Yasmin Barbosa',
|
|
180
|
-
'Zeca Mendes',
|
|
181
|
-
'Amanda Torres',
|
|
182
|
-
'Bruno Lopes',
|
|
183
|
-
'Camila Ramos',
|
|
184
|
-
'Diego Farias',
|
|
185
|
-
'Eduarda Moreira',
|
|
186
|
-
];
|
|
187
|
-
return nomes.map((nome, i) => ({
|
|
188
|
-
id: i + 1,
|
|
189
|
-
nome,
|
|
190
|
-
email: `${nome.toLowerCase().replace(' ', '.')}@email.com`,
|
|
191
|
-
telefone: `(11) 9${Math.floor(Math.random() * 9000 + 1000)}-${Math.floor(Math.random() * 9000 + 1000)}`,
|
|
192
|
-
matriculadoEm: `2024-0${Math.floor(Math.random() * 2 + 1)}-${String(Math.floor(Math.random() * 28 + 1)).padStart(2, '0')}`,
|
|
193
|
-
progresso: Math.floor(Math.random() * 60 + 40),
|
|
194
|
-
presenca: Math.floor(Math.random() * 30 + 70),
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function generateAulas(): Aula[] {
|
|
199
|
-
const today = new Date();
|
|
200
|
-
const aulas: Aula[] = [];
|
|
201
|
-
const titulos = [
|
|
202
|
-
'Introducao a Hooks',
|
|
203
|
-
'useEffect Avancado',
|
|
204
|
-
'Context API',
|
|
205
|
-
'Redux vs Zustand',
|
|
206
|
-
'Performance Optimization',
|
|
207
|
-
'React Query',
|
|
208
|
-
'Testing com Jest',
|
|
209
|
-
'Storybook',
|
|
210
|
-
'Next.js Fundamentos',
|
|
211
|
-
'SSR vs SSG',
|
|
212
|
-
'API Routes',
|
|
213
|
-
'Deploy e CI/CD',
|
|
214
|
-
];
|
|
215
|
-
for (let i = -10; i < 20; i++) {
|
|
216
|
-
const dia = addDays(today, i);
|
|
217
|
-
if (dia.getDay() === 0 || dia.getDay() === 6) continue;
|
|
218
|
-
const titulo = titulos[aulas.length % titulos.length]!;
|
|
219
|
-
aulas.push({
|
|
220
|
-
id: aulas.length + 1,
|
|
221
|
-
titulo,
|
|
222
|
-
data: dia,
|
|
223
|
-
horaInicio: '19:00',
|
|
224
|
-
horaFim: '22:00',
|
|
225
|
-
local: i % 3 === 0 ? 'Sala 201' : 'https://meet.google.com/abc-defg-hij',
|
|
226
|
-
tipo: i % 3 === 0 ? 'presencial' : 'online',
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
return aulas;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const ALUNOS_DISPONIVEIS = [
|
|
233
|
-
{ id: 101, nome: 'Fernando Moura', email: 'fernando.moura@email.com' },
|
|
234
|
-
{ id: 102, nome: 'Juliana Cardoso', email: 'juliana.cardoso@email.com' },
|
|
235
|
-
{ id: 103, nome: 'Roberto Freitas', email: 'roberto.freitas@email.com' },
|
|
236
|
-
{ id: 104, nome: 'Simone Andrade', email: 'simone.andrade@email.com' },
|
|
237
|
-
{ id: 105, nome: 'Thiago Monteiro', email: 'thiago.monteiro@email.com' },
|
|
238
|
-
];
|
|
576
|
+
// (mock data removed – all data comes from the API)
|
|
239
577
|
|
|
240
578
|
// ── Main Component ────────────────────────────────────────────────────────────
|
|
241
579
|
|
|
@@ -247,7 +585,6 @@ export default function TurmaDetalhePage() {
|
|
|
247
585
|
const router = useRouter();
|
|
248
586
|
const id = params.id as string;
|
|
249
587
|
const dateLocale = locale === 'pt' ? ptBR : enUS;
|
|
250
|
-
const calendarCulture = locale === 'pt' ? 'pt-BR' : 'en-US';
|
|
251
588
|
|
|
252
589
|
const calendarMessages = {
|
|
253
590
|
today: t('calendar.today'),
|
|
@@ -264,38 +601,196 @@ export default function TurmaDetalhePage() {
|
|
|
264
601
|
showMore: (count: number) => t('calendar.showMore', { count }),
|
|
265
602
|
};
|
|
266
603
|
|
|
267
|
-
//
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
604
|
+
// ── API ───────────────────────────────────────────────────────────────────
|
|
605
|
+
const { request } = useApp();
|
|
606
|
+
|
|
607
|
+
const {
|
|
608
|
+
data: turma,
|
|
609
|
+
isLoading: loadingTurma,
|
|
610
|
+
refetch: refetchTurma,
|
|
611
|
+
} = useQuery<ClassDetail>({
|
|
612
|
+
queryKey: ['lms-class', id],
|
|
613
|
+
queryFn: async () => {
|
|
614
|
+
const res = await request<ClassDetail>({
|
|
615
|
+
url: `/lms/classes/${id}`,
|
|
616
|
+
method: 'GET',
|
|
617
|
+
});
|
|
618
|
+
return res.data;
|
|
619
|
+
},
|
|
620
|
+
enabled: !!id,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const {
|
|
624
|
+
data: alunos = [],
|
|
625
|
+
isLoading: loadingAlunos,
|
|
626
|
+
refetch: refetchAlunos,
|
|
627
|
+
} = useQuery<Aluno[]>({
|
|
628
|
+
queryKey: ['lms-class-students', id],
|
|
629
|
+
queryFn: async () => {
|
|
630
|
+
const res = await request<Aluno[]>({
|
|
631
|
+
url: `/lms/classes/${id}/students`,
|
|
632
|
+
method: 'GET',
|
|
633
|
+
});
|
|
634
|
+
return res.data;
|
|
635
|
+
},
|
|
636
|
+
enabled: !!id,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const {
|
|
640
|
+
data: aulasQuery = EMPTY_AULAS,
|
|
641
|
+
isLoading: loadingAulas,
|
|
642
|
+
refetch: refetchAulas,
|
|
643
|
+
} = useQuery<Aula[]>({
|
|
644
|
+
queryKey: ['lms-class-sessions', id],
|
|
645
|
+
queryFn: async () => {
|
|
646
|
+
const res = await request<Aula[]>({
|
|
647
|
+
url: `/lms/classes/${id}/sessions`,
|
|
648
|
+
method: 'GET',
|
|
649
|
+
});
|
|
650
|
+
return res.data;
|
|
651
|
+
},
|
|
652
|
+
enabled: !!id,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const loading = loadingTurma || loadingAlunos || loadingAulas;
|
|
272
656
|
|
|
273
|
-
//
|
|
657
|
+
// ── Edit sheet state ──────────────────────────────────────────────────────
|
|
658
|
+
const [editSheetOpen, setEditSheetOpen] = useState(false);
|
|
659
|
+
|
|
660
|
+
// ── Tab state ─────────────────────────────────────────────────────────────
|
|
274
661
|
const [activeTab, setActiveTab] = useState('alunos');
|
|
662
|
+
const [calendarViewMode, setCalendarViewMode] = useState<
|
|
663
|
+
'single' | 'quarter' | 'year' | 'list'
|
|
664
|
+
>('single');
|
|
665
|
+
const [calendarViewDate, setCalendarViewDate] = useState(() => new Date());
|
|
275
666
|
|
|
276
|
-
//
|
|
667
|
+
// ── Students state ────────────────────────────────────────────────────────
|
|
277
668
|
const [alunoSearch, setAlunoSearch] = useState('');
|
|
278
669
|
const [selectedAlunos, setSelectedAlunos] = useState<number[]>([]);
|
|
279
|
-
const [addAlunoDialogOpen, setAddAlunoDialogOpen] = useState(false);
|
|
280
670
|
const [removeAlunoDialogOpen, setRemoveAlunoDialogOpen] = useState(false);
|
|
281
671
|
const [alunoToRemove, setAlunoToRemove] = useState<Aluno | null>(null);
|
|
282
|
-
const [
|
|
672
|
+
const [pessoaSearch, setPessoaSearch] = useState('');
|
|
673
|
+
const [studentPickerOpen, setStudentPickerOpen] = useState(false);
|
|
674
|
+
const [studentPickerResults, setStudentPickerResults] = useState<Person[]>(
|
|
675
|
+
[]
|
|
676
|
+
);
|
|
677
|
+
const [selectedPessoas, setSelectedPessoas] = useState<number[]>([]);
|
|
678
|
+
const [savingStudents, setSavingStudents] = useState(false);
|
|
679
|
+
const [removingStudent, setRemovingStudent] = useState(false);
|
|
680
|
+
const [createStudentDialogOpen, setCreateStudentDialogOpen] = useState(false);
|
|
681
|
+
const [studentProfileDialogOpen, setStudentProfileDialogOpen] =
|
|
682
|
+
useState(false);
|
|
683
|
+
const [editStudentSheetOpen, setEditStudentSheetOpen] = useState(false);
|
|
684
|
+
const [selectedStudentProfile, setSelectedStudentProfile] =
|
|
685
|
+
useState<StudentProfile | null>(null);
|
|
686
|
+
const [loadingStudentProfile, setLoadingStudentProfile] = useState(false);
|
|
687
|
+
const [savingStudentProfile, setSavingStudentProfile] = useState(false);
|
|
688
|
+
|
|
689
|
+
const { data: pessoasDisponiveis = [], isLoading: searchingPessoas } =
|
|
690
|
+
useQuery<Person[]>({
|
|
691
|
+
queryKey: ['lms-people-search', id, pessoaSearch],
|
|
692
|
+
queryFn: async () => {
|
|
693
|
+
const res = await request<Person[]>({
|
|
694
|
+
url: `/lms/classes/${id}/people/search`,
|
|
695
|
+
method: 'GET',
|
|
696
|
+
params: { q: pessoaSearch },
|
|
697
|
+
});
|
|
698
|
+
return res.data;
|
|
699
|
+
},
|
|
700
|
+
enabled: studentPickerOpen,
|
|
701
|
+
placeholderData: (previous) => previous ?? [],
|
|
702
|
+
});
|
|
283
703
|
|
|
284
|
-
//
|
|
285
|
-
const [calendarView, setCalendarView] = useState<View>('month');
|
|
286
|
-
const [calendarDate, setCalendarDate] = useState(new Date());
|
|
704
|
+
// ── Calendar state ────────────────────────────────────────────────────────
|
|
287
705
|
const [aulaSheetOpen, setAulaSheetOpen] = useState(false);
|
|
706
|
+
const [aulaSheetTab, setAulaSheetTab] = useState<'aulas' | 'chamada'>(
|
|
707
|
+
'aulas'
|
|
708
|
+
);
|
|
288
709
|
const [editingAula, setEditingAula] = useState<Aula | null>(null);
|
|
289
|
-
const [
|
|
290
|
-
|
|
710
|
+
const [savingAula, setSavingAula] = useState(false);
|
|
711
|
+
const [instructorOpen, setInstructorOpen] = useState(false);
|
|
712
|
+
const [instructorSearch, setInstructorSearch] = useState('');
|
|
713
|
+
const [createInstructorDialogOpen, setCreateInstructorDialogOpen] =
|
|
714
|
+
useState(false);
|
|
715
|
+
|
|
716
|
+
const {
|
|
717
|
+
data: instructorOptions = [],
|
|
718
|
+
isFetching: loadingInstructors,
|
|
719
|
+
refetch: refetchInstructorOptions,
|
|
720
|
+
} = useQuery<InstructorOption[]>({
|
|
721
|
+
queryKey: ['lms-class-session-instructors', id, instructorSearch],
|
|
722
|
+
queryFn: async () => {
|
|
723
|
+
const response = await request<
|
|
724
|
+
| InstructorApiRow[]
|
|
725
|
+
| {
|
|
726
|
+
data?: InstructorApiRow[];
|
|
727
|
+
items?: InstructorApiRow[];
|
|
728
|
+
rows?: InstructorApiRow[];
|
|
729
|
+
}
|
|
730
|
+
>({
|
|
731
|
+
url: '/lms/instructors',
|
|
732
|
+
method: 'GET',
|
|
733
|
+
params: {
|
|
734
|
+
page: 1,
|
|
735
|
+
pageSize: 100,
|
|
736
|
+
qualificationSlugs: ['class-sessions'],
|
|
737
|
+
...(instructorSearch.trim()
|
|
738
|
+
? { search: instructorSearch.trim() }
|
|
739
|
+
: {}),
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const payload = response.data;
|
|
744
|
+
const rows = Array.isArray(payload)
|
|
745
|
+
? payload
|
|
746
|
+
: Array.isArray(payload?.data)
|
|
747
|
+
? payload.data
|
|
748
|
+
: Array.isArray(payload?.items)
|
|
749
|
+
? payload.items
|
|
750
|
+
: Array.isArray(payload?.rows)
|
|
751
|
+
? payload.rows
|
|
752
|
+
: [];
|
|
753
|
+
|
|
754
|
+
const unique = new Map<number, InstructorOption>();
|
|
755
|
+
|
|
756
|
+
for (const row of rows) {
|
|
757
|
+
const normalized = normalizeInstructorOption(row);
|
|
758
|
+
if (!normalized) continue;
|
|
759
|
+
unique.set(normalized.id, normalized);
|
|
760
|
+
}
|
|
291
761
|
|
|
292
|
-
|
|
293
|
-
|
|
762
|
+
return Array.from(unique.values()).sort((a, b) =>
|
|
763
|
+
a.name.localeCompare(b.name)
|
|
764
|
+
);
|
|
765
|
+
},
|
|
766
|
+
initialData: [],
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
if (aulaSheetOpen || instructorOpen) {
|
|
771
|
+
void refetchInstructorOptions();
|
|
772
|
+
}
|
|
773
|
+
}, [aulaSheetOpen, instructorOpen, refetchInstructorOptions]);
|
|
774
|
+
|
|
775
|
+
// ── Attendance state ──────────────────────────────────────────────────────
|
|
294
776
|
const [presencaList, setPresencaList] = useState<PresencaItem[]>([]);
|
|
295
777
|
const [savingPresenca, setSavingPresenca] = useState(false);
|
|
778
|
+
const [aulasState, setAulasState] = useState<Aula[]>([]);
|
|
779
|
+
|
|
780
|
+
useEffect(() => {
|
|
781
|
+
setAulasState(
|
|
782
|
+
[...aulasQuery].sort(
|
|
783
|
+
(a, b) =>
|
|
784
|
+
parseSessionDate(a.data).getTime() -
|
|
785
|
+
parseSessionDate(b.data).getTime()
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
}, [aulasQuery]);
|
|
296
789
|
|
|
297
|
-
// Form
|
|
790
|
+
// ── Form ──────────────────────────────────────────────────────────────────
|
|
298
791
|
const aulaSchema = getAulaSchema(t);
|
|
792
|
+
const studentSchema = getStudentSchema(t);
|
|
793
|
+
|
|
299
794
|
const aulaForm = useForm<AulaForm>({
|
|
300
795
|
resolver: zodResolver(aulaSchema),
|
|
301
796
|
defaultValues: {
|
|
@@ -305,20 +800,29 @@ export default function TurmaDetalhePage() {
|
|
|
305
800
|
horaFim: '22:00',
|
|
306
801
|
local: '',
|
|
307
802
|
tipo: 'online',
|
|
803
|
+
instrutorId: '',
|
|
804
|
+
recurrenceFrequency: 'none',
|
|
805
|
+
recurrenceUntil: '',
|
|
806
|
+
recurrenceDaysOfWeek: [],
|
|
807
|
+
cor: SESSION_DEFAULT_COLOR,
|
|
808
|
+
applyScope: 'single',
|
|
308
809
|
},
|
|
309
810
|
});
|
|
310
811
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
812
|
+
const recurrenceFrequency = aulaForm.watch('recurrenceFrequency');
|
|
813
|
+
const recurrenceDaysOfWeek = aulaForm.watch('recurrenceDaysOfWeek') ?? [];
|
|
814
|
+
const showRecurrenceFields = !editingAula || editingAula.isRecurring;
|
|
815
|
+
|
|
816
|
+
const editStudentForm = useForm<StudentForm>({
|
|
817
|
+
resolver: zodResolver(studentSchema),
|
|
818
|
+
defaultValues: {
|
|
819
|
+
name: '',
|
|
820
|
+
email: '',
|
|
821
|
+
phone: '',
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// ── Derived ───────────────────────────────────────────────────────────────
|
|
322
826
|
const filteredAlunos = useMemo(() => {
|
|
323
827
|
if (!alunoSearch.trim()) return alunos;
|
|
324
828
|
const q = alunoSearch.toLowerCase();
|
|
@@ -328,257 +832,649 @@ export default function TurmaDetalhePage() {
|
|
|
328
832
|
);
|
|
329
833
|
}, [alunos, alunoSearch]);
|
|
330
834
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.
|
|
336
|
-
.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}, [aulas]);
|
|
347
|
-
|
|
348
|
-
// Event style
|
|
349
|
-
const eventStyleGetter = useCallback(
|
|
350
|
-
(event: { resource?: Aula }) => ({
|
|
351
|
-
style: {
|
|
352
|
-
backgroundColor:
|
|
353
|
-
event.resource?.tipo === 'presencial' ? '#3b82f6' : '#22c55e',
|
|
354
|
-
border: 'none',
|
|
355
|
-
borderRadius: '6px',
|
|
356
|
-
color: '#fff',
|
|
357
|
-
fontSize: '0.75rem',
|
|
358
|
-
fontWeight: 500,
|
|
359
|
-
padding: '3px 8px',
|
|
360
|
-
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
|
|
361
|
-
},
|
|
362
|
-
}),
|
|
363
|
-
[]
|
|
835
|
+
const pessoasElegiveisParaMatricula = useMemo(
|
|
836
|
+
() =>
|
|
837
|
+
pessoasDisponiveis.filter((person) => {
|
|
838
|
+
const isInstructor = Boolean(person.isInstructor);
|
|
839
|
+
const canTeachCourses = Boolean(person.canTeachCourses);
|
|
840
|
+
const isStudentByEnrollment = Boolean(person.isStudentByEnrollment);
|
|
841
|
+
|
|
842
|
+
// Teacher-only profiles stay out of the student enrollment picker.
|
|
843
|
+
return !(isInstructor && canTeachCourses && !isStudentByEnrollment);
|
|
844
|
+
}),
|
|
845
|
+
[pessoasDisponiveis]
|
|
846
|
+
);
|
|
847
|
+
const studentPickerResultsById = useMemo(
|
|
848
|
+
() => new Map(studentPickerResults.map((person) => [person.id, person])),
|
|
849
|
+
[studentPickerResults]
|
|
364
850
|
);
|
|
365
851
|
|
|
366
|
-
|
|
367
|
-
|
|
852
|
+
useEffect(() => {
|
|
853
|
+
if (pessoasElegiveisParaMatricula.length === 0) return;
|
|
854
|
+
|
|
855
|
+
setStudentPickerResults((previous) => {
|
|
856
|
+
const merged = new Map(previous.map((person) => [person.id, person]));
|
|
857
|
+
|
|
858
|
+
for (const person of pessoasElegiveisParaMatricula) {
|
|
859
|
+
merged.set(person.id, person);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return Array.from(merged.values());
|
|
863
|
+
});
|
|
864
|
+
}, [pessoasElegiveisParaMatricula]);
|
|
865
|
+
|
|
866
|
+
const calendarEvents = useMemo(() => {
|
|
867
|
+
return aulasState.map((aula) => ({
|
|
868
|
+
id: aula.id,
|
|
869
|
+
title: aula.titulo,
|
|
870
|
+
start: getSessionStartDate(aula),
|
|
871
|
+
end: getSessionEndDate(aula),
|
|
872
|
+
resource: aula,
|
|
873
|
+
}));
|
|
874
|
+
}, [aulasState]);
|
|
875
|
+
|
|
876
|
+
const sessionsByDay = useMemo(() => {
|
|
877
|
+
const map = new Map<string, Aula[]>();
|
|
878
|
+
for (const aula of aulasState) {
|
|
879
|
+
const key = format(parseSessionDate(aula.data), 'yyyy-MM-dd');
|
|
880
|
+
if (!map.has(key)) map.set(key, []);
|
|
881
|
+
map.get(key)!.push(aula);
|
|
882
|
+
}
|
|
883
|
+
return map;
|
|
884
|
+
}, [aulasState]);
|
|
885
|
+
|
|
886
|
+
const notifyLmsDataUpdated = () => {
|
|
887
|
+
if (typeof window === 'undefined') return;
|
|
888
|
+
|
|
889
|
+
window.sessionStorage.setItem('lms:classes-needs-refresh', '1');
|
|
890
|
+
window.dispatchEvent(new CustomEvent('lms:classes-updated'));
|
|
891
|
+
window.sessionStorage.setItem('lms:dashboard-needs-refresh', '1');
|
|
892
|
+
window.dispatchEvent(new CustomEvent('lms:dashboard-updated'));
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// ── Handlers ─────────────────────────────────────────────────────────────
|
|
896
|
+
const toggleSelectAluno = (studentId: number, e?: React.MouseEvent) => {
|
|
368
897
|
if (e?.shiftKey && selectedAlunos.length > 0) {
|
|
369
898
|
const lastSelected = selectedAlunos[selectedAlunos.length - 1];
|
|
370
899
|
const lastIndex = filteredAlunos.findIndex((a) => a.id === lastSelected);
|
|
371
|
-
const currentIndex = filteredAlunos.findIndex((a) => a.id ===
|
|
900
|
+
const currentIndex = filteredAlunos.findIndex((a) => a.id === studentId);
|
|
372
901
|
const [start, end] = [
|
|
373
902
|
Math.min(lastIndex, currentIndex),
|
|
374
903
|
Math.max(lastIndex, currentIndex),
|
|
375
904
|
];
|
|
376
905
|
const range = filteredAlunos.slice(start, end + 1).map((a) => a.id);
|
|
377
906
|
setSelectedAlunos((prev) => [...new Set([...prev, ...range])]);
|
|
378
|
-
} else if (e?.ctrlKey || e?.metaKey) {
|
|
379
|
-
setSelectedAlunos((prev) =>
|
|
380
|
-
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
381
|
-
);
|
|
382
907
|
} else {
|
|
383
908
|
setSelectedAlunos((prev) =>
|
|
384
|
-
prev.includes(
|
|
909
|
+
prev.includes(studentId)
|
|
910
|
+
? prev.filter((x) => x !== studentId)
|
|
911
|
+
: [...prev, studentId]
|
|
385
912
|
);
|
|
386
913
|
}
|
|
387
914
|
};
|
|
388
915
|
|
|
389
|
-
const handleAddAlunos = () => {
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
916
|
+
const handleAddAlunos = async () => {
|
|
917
|
+
if (selectedPessoas.length === 0) return;
|
|
918
|
+
|
|
919
|
+
const selectedEligiblePeople = selectedPessoas.filter((personId) =>
|
|
920
|
+
studentPickerResultsById.has(personId)
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
if (selectedEligiblePeople.length === 0) {
|
|
924
|
+
toast.error('Nenhuma pessoa elegivel foi encontrada para matricula.');
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
setSavingStudents(true);
|
|
929
|
+
try {
|
|
930
|
+
await Promise.all(
|
|
931
|
+
selectedEligiblePeople.map((personId) =>
|
|
932
|
+
request({
|
|
933
|
+
url: `/lms/classes/${id}/students`,
|
|
934
|
+
method: 'POST',
|
|
935
|
+
data: { personId },
|
|
936
|
+
})
|
|
937
|
+
)
|
|
938
|
+
);
|
|
939
|
+
await refetchAlunos();
|
|
940
|
+
notifyLmsDataUpdated();
|
|
941
|
+
setStudentPickerOpen(false);
|
|
942
|
+
setStudentPickerResults([]);
|
|
943
|
+
setSelectedPessoas([]);
|
|
944
|
+
setPessoaSearch('');
|
|
945
|
+
toast.success(
|
|
946
|
+
t('toasts.studentsAdded', { count: selectedEligiblePeople.length })
|
|
947
|
+
);
|
|
948
|
+
} catch (error: unknown) {
|
|
949
|
+
const message = getErrorMessage(error);
|
|
950
|
+
|
|
951
|
+
if (message?.toLowerCase().includes('already enrolled')) {
|
|
952
|
+
toast.error('Esta pessoa ja esta matriculada na turma.');
|
|
953
|
+
} else {
|
|
954
|
+
toast.error(message || t('toasts.error'));
|
|
955
|
+
}
|
|
956
|
+
} finally {
|
|
957
|
+
setSavingStudents(false);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const handleStudentEnrolled = async () => {
|
|
962
|
+
await refetchAlunos();
|
|
963
|
+
notifyLmsDataUpdated();
|
|
964
|
+
setCreateStudentDialogOpen(false);
|
|
965
|
+
setStudentPickerOpen(false);
|
|
966
|
+
setStudentPickerResults([]);
|
|
967
|
+
setSelectedPessoas([]);
|
|
968
|
+
setPessoaSearch('');
|
|
969
|
+
toast.success(t('toasts.studentCreated'));
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
const openCreateStudentDialog = () => {
|
|
973
|
+
setStudentPickerOpen(false);
|
|
974
|
+
setPessoaSearch('');
|
|
975
|
+
setCreateStudentDialogOpen(true);
|
|
404
976
|
};
|
|
405
977
|
|
|
406
|
-
const handleRemoveAluno = () => {
|
|
978
|
+
const handleRemoveAluno = async () => {
|
|
407
979
|
if (!alunoToRemove) return;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
980
|
+
setRemovingStudent(true);
|
|
981
|
+
try {
|
|
982
|
+
await request({
|
|
983
|
+
url: `/lms/classes/${id}/students/${alunoToRemove.id}`,
|
|
984
|
+
method: 'DELETE',
|
|
985
|
+
});
|
|
986
|
+
await refetchAlunos();
|
|
987
|
+
notifyLmsDataUpdated();
|
|
988
|
+
setRemoveAlunoDialogOpen(false);
|
|
989
|
+
setAlunoToRemove(null);
|
|
990
|
+
toast.success(t('toasts.studentRemoved'));
|
|
991
|
+
} catch {
|
|
992
|
+
toast.error(t('toasts.error'));
|
|
993
|
+
} finally {
|
|
994
|
+
setRemovingStudent(false);
|
|
995
|
+
}
|
|
412
996
|
};
|
|
413
997
|
|
|
414
|
-
const handleRemoveSelectedAlunos = () => {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
998
|
+
const handleRemoveSelectedAlunos = async () => {
|
|
999
|
+
if (selectedAlunos.length === 0) return;
|
|
1000
|
+
try {
|
|
1001
|
+
await Promise.all(
|
|
1002
|
+
selectedAlunos.map((personId) =>
|
|
1003
|
+
request({
|
|
1004
|
+
url: `/lms/classes/${id}/students/${personId}`,
|
|
1005
|
+
method: 'DELETE',
|
|
1006
|
+
})
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
1009
|
+
await refetchAlunos();
|
|
1010
|
+
notifyLmsDataUpdated();
|
|
1011
|
+
setSelectedAlunos([]);
|
|
1012
|
+
toast.success(
|
|
1013
|
+
t('toasts.studentsRemoved', { count: selectedAlunos.length })
|
|
1014
|
+
);
|
|
1015
|
+
} catch {
|
|
1016
|
+
toast.error(t('toasts.error'));
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
const openStudentProfile = async (personId: number) => {
|
|
1021
|
+
setLoadingStudentProfile(true);
|
|
1022
|
+
try {
|
|
1023
|
+
const res = await request<StudentProfile>({
|
|
1024
|
+
url: `/lms/classes/${id}/students/${personId}`,
|
|
1025
|
+
method: 'GET',
|
|
1026
|
+
});
|
|
1027
|
+
const profile = res.data;
|
|
1028
|
+
setSelectedStudentProfile(profile);
|
|
1029
|
+
setStudentProfileDialogOpen(true);
|
|
1030
|
+
} catch {
|
|
1031
|
+
toast.error(t('toasts.error'));
|
|
1032
|
+
} finally {
|
|
1033
|
+
setLoadingStudentProfile(false);
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const openEditStudentSheet = () => {
|
|
1038
|
+
if (!selectedStudentProfile) return;
|
|
1039
|
+
editStudentForm.reset({
|
|
1040
|
+
name: selectedStudentProfile.nome || '',
|
|
1041
|
+
email: selectedStudentProfile.email || '',
|
|
1042
|
+
phone: selectedStudentProfile.telefone || '',
|
|
1043
|
+
});
|
|
1044
|
+
setStudentProfileDialogOpen(false);
|
|
1045
|
+
setEditStudentSheetOpen(true);
|
|
420
1046
|
};
|
|
421
1047
|
|
|
422
|
-
const
|
|
1048
|
+
const handleUpdateStudentProfile = editStudentForm.handleSubmit(
|
|
1049
|
+
async (values) => {
|
|
1050
|
+
if (!selectedStudentProfile) return;
|
|
1051
|
+
|
|
1052
|
+
setSavingStudentProfile(true);
|
|
1053
|
+
try {
|
|
1054
|
+
const res = await request<StudentProfile>({
|
|
1055
|
+
url: `/lms/classes/${id}/students/${selectedStudentProfile.id}`,
|
|
1056
|
+
method: 'PATCH',
|
|
1057
|
+
data: {
|
|
1058
|
+
name: values.name,
|
|
1059
|
+
email: values.email,
|
|
1060
|
+
phone: values.phone,
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
setSelectedStudentProfile(res.data);
|
|
1065
|
+
await refetchAlunos();
|
|
1066
|
+
setEditStudentSheetOpen(false);
|
|
1067
|
+
setStudentProfileDialogOpen(true);
|
|
1068
|
+
toast.success(t('toasts.studentUpdated'));
|
|
1069
|
+
} catch {
|
|
1070
|
+
toast.error(t('toasts.error'));
|
|
1071
|
+
} finally {
|
|
1072
|
+
setSavingStudentProfile(false);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
const getDefaultPresencaList = useCallback(
|
|
1078
|
+
() =>
|
|
1079
|
+
alunos.map((a) => ({
|
|
1080
|
+
alunoId: a.id,
|
|
1081
|
+
selecionado: false,
|
|
1082
|
+
presente: false,
|
|
1083
|
+
})),
|
|
1084
|
+
[alunos]
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
const loadPresencaForAula = useCallback(
|
|
1088
|
+
async (aula: Aula) => {
|
|
1089
|
+
setPresencaList(getDefaultPresencaList());
|
|
1090
|
+
try {
|
|
1091
|
+
const res = await request<AttendanceRecord[]>({
|
|
1092
|
+
url: `/lms/classes/${id}/sessions/${aula.id}/attendance`,
|
|
1093
|
+
method: 'GET',
|
|
1094
|
+
});
|
|
1095
|
+
const attendanceData = res.data;
|
|
1096
|
+
setPresencaList(
|
|
1097
|
+
alunos.map((a) => {
|
|
1098
|
+
const found = attendanceData.find((att) => att.student_id === a.id);
|
|
1099
|
+
return {
|
|
1100
|
+
alunoId: a.id,
|
|
1101
|
+
selecionado: Boolean(found),
|
|
1102
|
+
presente: found?.present ?? false,
|
|
1103
|
+
};
|
|
1104
|
+
})
|
|
1105
|
+
);
|
|
1106
|
+
} catch {
|
|
1107
|
+
// Keep default values if attendance cannot be loaded.
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
[alunos, getDefaultPresencaList, id, request]
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
const openAulaSheet = (aula?: Aula, options?: OpenAulaSheetOptions) => {
|
|
1114
|
+
setAulaSheetTab(options?.initialTab ?? 'aulas');
|
|
1115
|
+
|
|
423
1116
|
if (aula) {
|
|
424
1117
|
setEditingAula(aula);
|
|
425
1118
|
aulaForm.reset({
|
|
426
1119
|
titulo: aula.titulo,
|
|
427
|
-
data:
|
|
1120
|
+
data: toSessionInputDate(aula.data),
|
|
428
1121
|
horaInicio: aula.horaInicio,
|
|
429
1122
|
horaFim: aula.horaFim,
|
|
430
|
-
local: aula.local,
|
|
1123
|
+
local: aula.local || aula.meetingUrl || '',
|
|
431
1124
|
tipo: aula.tipo,
|
|
1125
|
+
instrutorId: aula.instructorId ? String(aula.instructorId) : '',
|
|
1126
|
+
recurrenceFrequency: aula.recurrence?.frequency ?? 'none',
|
|
1127
|
+
recurrenceUntil: aula.recurrence?.until ?? '',
|
|
1128
|
+
recurrenceDaysOfWeek: aula.recurrence?.daysOfWeek ?? [],
|
|
1129
|
+
cor: getAulaDisplayColor(aula),
|
|
1130
|
+
applyScope: 'single',
|
|
432
1131
|
});
|
|
1132
|
+
void loadPresencaForAula(aula);
|
|
433
1133
|
} else {
|
|
434
1134
|
setEditingAula(null);
|
|
435
1135
|
aulaForm.reset({
|
|
436
1136
|
titulo: '',
|
|
437
1137
|
data: '',
|
|
438
|
-
horaInicio: '19:00',
|
|
439
|
-
horaFim: '22:00',
|
|
1138
|
+
horaInicio: turma?.startTime ?? '19:00',
|
|
1139
|
+
horaFim: turma?.endTime ?? '22:00',
|
|
440
1140
|
local: '',
|
|
441
|
-
tipo: 'online',
|
|
1141
|
+
tipo: turma?.deliveryMode === 'presential' ? 'presencial' : 'online',
|
|
1142
|
+
instrutorId: '',
|
|
1143
|
+
recurrenceFrequency: 'none',
|
|
1144
|
+
recurrenceUntil: '',
|
|
1145
|
+
recurrenceDaysOfWeek: [],
|
|
1146
|
+
cor: SESSION_DEFAULT_COLOR,
|
|
1147
|
+
applyScope: 'single',
|
|
1148
|
+
...options?.prefill,
|
|
442
1149
|
});
|
|
1150
|
+
setPresencaList(options?.attendance ?? getDefaultPresencaList());
|
|
443
1151
|
}
|
|
1152
|
+
|
|
444
1153
|
setAulaSheetOpen(true);
|
|
445
1154
|
};
|
|
446
1155
|
|
|
447
|
-
const
|
|
1156
|
+
const upsertAula = async (data: AulaForm) => {
|
|
1157
|
+
const isOnline = data.tipo === 'online';
|
|
1158
|
+
const payload = {
|
|
1159
|
+
title: data.titulo,
|
|
1160
|
+
sessionDate: data.data,
|
|
1161
|
+
startTime: data.horaInicio,
|
|
1162
|
+
endTime: data.horaFim,
|
|
1163
|
+
location: isOnline ? undefined : data.local,
|
|
1164
|
+
meetingUrl: isOnline ? data.local : undefined,
|
|
1165
|
+
instructorId: data.instrutorId ? Number(data.instrutorId) : undefined,
|
|
1166
|
+
color: data.cor,
|
|
1167
|
+
recurrence: showRecurrenceFields
|
|
1168
|
+
? buildSessionRecurrence(data)
|
|
1169
|
+
: undefined,
|
|
1170
|
+
applyScope: editingAula?.isRecurring ? data.applyScope : undefined,
|
|
1171
|
+
};
|
|
1172
|
+
|
|
448
1173
|
if (editingAula) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
horaInicio: data.horaInicio,
|
|
457
|
-
horaFim: data.horaFim,
|
|
458
|
-
local: data.local,
|
|
459
|
-
tipo: data.tipo as 'presencial' | 'online',
|
|
460
|
-
}
|
|
461
|
-
: a
|
|
462
|
-
)
|
|
1174
|
+
const res = await request<Aula>({
|
|
1175
|
+
url: `/lms/classes/${id}/sessions/${editingAula.id}`,
|
|
1176
|
+
method: 'PATCH',
|
|
1177
|
+
data: payload,
|
|
1178
|
+
});
|
|
1179
|
+
setAulasState((prev) =>
|
|
1180
|
+
prev.map((item) => (item.id === editingAula.id ? res.data : item))
|
|
463
1181
|
);
|
|
464
|
-
|
|
465
|
-
} else {
|
|
466
|
-
const newAula: Aula = {
|
|
467
|
-
id: Math.max(...aulas.map((a) => a.id), 0) + 1,
|
|
468
|
-
titulo: data.titulo,
|
|
469
|
-
data: new Date(data.data),
|
|
470
|
-
horaInicio: data.horaInicio,
|
|
471
|
-
horaFim: data.horaFim,
|
|
472
|
-
local: data.local,
|
|
473
|
-
tipo: data.tipo as 'presencial' | 'online',
|
|
474
|
-
};
|
|
475
|
-
setAulas((prev) => [...prev, newAula]);
|
|
476
|
-
toast.success(t('toasts.lessonCreated'));
|
|
1182
|
+
return { aula: res.data, mode: 'updated' as const };
|
|
477
1183
|
}
|
|
478
|
-
setAulaSheetOpen(false);
|
|
479
|
-
});
|
|
480
1184
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1185
|
+
const res = await request<Aula>({
|
|
1186
|
+
url: `/lms/classes/${id}/sessions`,
|
|
1187
|
+
method: 'POST',
|
|
1188
|
+
data: payload,
|
|
1189
|
+
});
|
|
1190
|
+
setAulasState((prev) =>
|
|
1191
|
+
[...prev, res.data].sort(
|
|
1192
|
+
(a, b) =>
|
|
1193
|
+
parseSessionDate(a.data).getTime() -
|
|
1194
|
+
parseSessionDate(b.data).getTime()
|
|
1195
|
+
)
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
return { aula: res.data, mode: 'created' as const };
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const savePresencaForAula = async (sessionId: number) => {
|
|
1202
|
+
await request({
|
|
1203
|
+
url: `/lms/classes/${id}/sessions/${sessionId}/attendance`,
|
|
1204
|
+
method: 'POST',
|
|
1205
|
+
data: {
|
|
1206
|
+
attendance: presencaList
|
|
1207
|
+
.filter((p) => p.selecionado)
|
|
1208
|
+
.map((p) => ({
|
|
1209
|
+
studentId: p.alunoId,
|
|
1210
|
+
present: p.presente,
|
|
1211
|
+
})),
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
const handleSaveAula = aulaForm.handleSubmit(async (data) => {
|
|
1217
|
+
setSavingAula(true);
|
|
1218
|
+
try {
|
|
1219
|
+
const { mode } = await upsertAula(data);
|
|
1220
|
+
toast.success(
|
|
1221
|
+
mode === 'updated'
|
|
1222
|
+
? t('toasts.lessonUpdated')
|
|
1223
|
+
: t('toasts.lessonCreated')
|
|
1224
|
+
);
|
|
1225
|
+
void refetchAulas();
|
|
1226
|
+
notifyLmsDataUpdated();
|
|
1227
|
+
setAulaSheetOpen(false);
|
|
1228
|
+
} catch {
|
|
1229
|
+
toast.error(t('toasts.error'));
|
|
1230
|
+
} finally {
|
|
1231
|
+
setSavingAula(false);
|
|
484
1232
|
}
|
|
485
|
-
}
|
|
1233
|
+
});
|
|
486
1234
|
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1235
|
+
const handleQuickCreateSaved = (aula: Aula) => {
|
|
1236
|
+
setAulasState((prev) =>
|
|
1237
|
+
[...prev, aula].sort(
|
|
1238
|
+
(a, b) =>
|
|
1239
|
+
parseSessionDate(a.data).getTime() -
|
|
1240
|
+
parseSessionDate(b.data).getTime()
|
|
1241
|
+
)
|
|
491
1242
|
);
|
|
492
|
-
|
|
1243
|
+
void refetchAulas();
|
|
1244
|
+
notifyLmsDataUpdated();
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
const handleInstructorCreated = async (instructor: {
|
|
1248
|
+
id: number;
|
|
1249
|
+
personId: number;
|
|
1250
|
+
name: string;
|
|
1251
|
+
qualificationSlugs: string[];
|
|
1252
|
+
}) => {
|
|
1253
|
+
aulaForm.setValue('instrutorId', String(instructor.id), {
|
|
1254
|
+
shouldDirty: true,
|
|
1255
|
+
shouldTouch: true,
|
|
1256
|
+
shouldValidate: true,
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
await refetchInstructorOptions();
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const openPresenca = (aula: Aula) => {
|
|
1263
|
+
openAulaSheet(aula, { initialTab: 'chamada' });
|
|
493
1264
|
};
|
|
494
1265
|
|
|
495
1266
|
const togglePresenca = (alunoId: number) => {
|
|
496
1267
|
setPresencaList((prev) =>
|
|
497
1268
|
prev.map((p) =>
|
|
498
|
-
p.alunoId === alunoId
|
|
1269
|
+
p.alunoId === alunoId
|
|
1270
|
+
? { ...p, selecionado: true, presente: !p.presente }
|
|
1271
|
+
: p
|
|
499
1272
|
)
|
|
500
1273
|
);
|
|
501
1274
|
};
|
|
502
1275
|
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1276
|
+
const toggleParticipante = (alunoId: number) => {
|
|
1277
|
+
setPresencaList((prev) =>
|
|
1278
|
+
prev.map((p) =>
|
|
1279
|
+
p.alunoId === alunoId
|
|
1280
|
+
? {
|
|
1281
|
+
...p,
|
|
1282
|
+
selecionado: !p.selecionado,
|
|
1283
|
+
presente: p.selecionado ? false : p.presente,
|
|
1284
|
+
}
|
|
1285
|
+
: p
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
509
1288
|
};
|
|
510
1289
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1290
|
+
const handleSaveAulaAndPresenca = aulaForm.handleSubmit(async (data) => {
|
|
1291
|
+
setSavingPresenca(true);
|
|
1292
|
+
setSavingAula(true);
|
|
1293
|
+
try {
|
|
1294
|
+
const { aula } = await upsertAula(data);
|
|
1295
|
+
if (!aula?.id) {
|
|
1296
|
+
throw new Error('Invalid session id');
|
|
1297
|
+
}
|
|
1298
|
+
await savePresencaForAula(aula.id);
|
|
1299
|
+
void refetchAulas();
|
|
1300
|
+
notifyLmsDataUpdated();
|
|
1301
|
+
setAulaSheetOpen(false);
|
|
1302
|
+
toast.success(t('toasts.attendanceSaved'));
|
|
1303
|
+
} catch {
|
|
1304
|
+
toast.error(t('toasts.error'));
|
|
1305
|
+
} finally {
|
|
1306
|
+
setSavingPresenca(false);
|
|
1307
|
+
setSavingAula(false);
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
const handleSaveAttendanceOnly = async () => {
|
|
1312
|
+
if (!editingAula) {
|
|
1313
|
+
await handleSaveAulaAndPresenca();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
setSavingPresenca(true);
|
|
1318
|
+
try {
|
|
1319
|
+
await savePresencaForAula(editingAula.id);
|
|
1320
|
+
void refetchAulas();
|
|
1321
|
+
notifyLmsDataUpdated();
|
|
1322
|
+
setAulaSheetOpen(false);
|
|
1323
|
+
toast.success(t('toasts.attendanceSaved'));
|
|
1324
|
+
} catch {
|
|
1325
|
+
toast.error(t('toasts.error'));
|
|
1326
|
+
} finally {
|
|
1327
|
+
setSavingPresenca(false);
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// ── KPIs ──────────────────────────────────────────────────────────────────
|
|
1332
|
+
const now = new Date();
|
|
1333
|
+
const completedSessions = aulasState.filter(
|
|
1334
|
+
(aula) => getSessionEndDate(aula) < now
|
|
1335
|
+
);
|
|
1336
|
+
const upcomingSessions = aulasState.filter(
|
|
1337
|
+
(aula) => getSessionEndDate(aula) >= now
|
|
1338
|
+
);
|
|
1339
|
+
const nextSession = upcomingSessions[0] ?? null;
|
|
1340
|
+
const completedSessionsCount = completedSessions.length;
|
|
1341
|
+
const totalSessionsCount = aulasState.length;
|
|
1342
|
+
const capacity = turma?.capacity ?? 0;
|
|
1343
|
+
const occupiedSeats = alunos.length;
|
|
1344
|
+
const availableSeats = Math.max(capacity - occupiedSeats, 0);
|
|
1345
|
+
const occupancyRate =
|
|
1346
|
+
capacity > 0 ? Math.round((occupiedSeats / capacity) * 100) : 0;
|
|
1347
|
+
const primaryInstructor =
|
|
1348
|
+
nextSession?.instructorName?.trim() ||
|
|
1349
|
+
aulasState
|
|
1350
|
+
.find((aula) => aula.instructorName?.trim())
|
|
1351
|
+
?.instructorName?.trim() ||
|
|
1352
|
+
'Nao definido';
|
|
1353
|
+
const nextSessionStartsAt = nextSession
|
|
1354
|
+
? getSessionStartDate(nextSession)
|
|
1355
|
+
: null;
|
|
1356
|
+
const nextSessionSummary = nextSessionStartsAt
|
|
1357
|
+
? format(nextSessionStartsAt, "dd/MM 'as' HH:mm", { locale: dateLocale })
|
|
1358
|
+
: 'Nenhuma aula futura';
|
|
1359
|
+
const nextSessionSupportText = nextSession
|
|
1360
|
+
? nextSession.titulo
|
|
1361
|
+
: 'Agende a proxima aula para manter a turma em movimento.';
|
|
1362
|
+
|
|
1363
|
+
const kpis: KpiCardItem[] = [
|
|
1364
|
+
{
|
|
1365
|
+
key: 'enrolled-students',
|
|
1366
|
+
title: t('kpis.enrolledStudents.label'),
|
|
1367
|
+
value: occupiedSeats,
|
|
1368
|
+
description: t('kpis.enrolledStudents.sub', {
|
|
1369
|
+
vagas: capacity,
|
|
1370
|
+
}),
|
|
1371
|
+
icon: Users,
|
|
1372
|
+
iconContainerClassName: 'bg-orange-500/10 text-orange-700',
|
|
1373
|
+
accentClassName: 'from-orange-500/25 via-amber-500/10 to-transparent',
|
|
1374
|
+
layout: 'compact',
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
key: 'occupancy-rate',
|
|
1378
|
+
title: t('kpis.occupancyRate.label'),
|
|
1379
|
+
value: capacity > 0 ? `${occupancyRate}%` : '—',
|
|
1380
|
+
description:
|
|
1381
|
+
capacity > 0 && availableSeats > 0
|
|
1382
|
+
? t('kpis.occupancyRate.subFree', {
|
|
1383
|
+
count: availableSeats,
|
|
1384
|
+
})
|
|
1385
|
+
: capacity > 0
|
|
1386
|
+
? t('kpis.occupancyRate.subFull')
|
|
1387
|
+
: 'Capacidade nao definida',
|
|
1388
|
+
icon: BarChart3,
|
|
1389
|
+
iconContainerClassName: 'bg-sky-500/10 text-sky-700',
|
|
1390
|
+
accentClassName: 'from-sky-500/25 via-blue-500/10 to-transparent',
|
|
1391
|
+
layout: 'compact',
|
|
533
1392
|
},
|
|
534
1393
|
{
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1394
|
+
key: 'next-session',
|
|
1395
|
+
title: 'Proxima aula',
|
|
1396
|
+
value: nextSession
|
|
1397
|
+
? format(nextSessionStartsAt!, 'dd/MM', { locale: dateLocale })
|
|
1398
|
+
: 'Sem aula',
|
|
1399
|
+
description: nextSession
|
|
1400
|
+
? `${nextSession.horaInicio} - ${nextSession.horaFim}`
|
|
1401
|
+
: 'Crie a proxima sessao',
|
|
1402
|
+
icon: Clock,
|
|
1403
|
+
iconContainerClassName: 'bg-emerald-500/10 text-emerald-700',
|
|
1404
|
+
accentClassName: 'from-emerald-500/25 via-green-500/10 to-transparent',
|
|
1405
|
+
layout: 'compact',
|
|
541
1406
|
},
|
|
542
1407
|
{
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1408
|
+
key: 'calendar',
|
|
1409
|
+
title: 'Calendario',
|
|
1410
|
+
value: totalSessionsCount,
|
|
1411
|
+
description: `${completedSessionsCount} concluidas`,
|
|
546
1412
|
icon: CalendarIcon,
|
|
547
|
-
|
|
548
|
-
|
|
1413
|
+
iconContainerClassName: 'bg-violet-500/10 text-violet-700',
|
|
1414
|
+
accentClassName: 'from-violet-500/25 via-violet-500/10 to-transparent',
|
|
1415
|
+
layout: 'compact',
|
|
549
1416
|
},
|
|
550
1417
|
];
|
|
551
1418
|
|
|
552
|
-
const STATUS_MAP = {
|
|
553
|
-
|
|
1419
|
+
const STATUS_MAP: Record<string, { label: string; color: string }> = {
|
|
1420
|
+
open: {
|
|
554
1421
|
label: tClasses('status.aberta'),
|
|
555
1422
|
color: 'bg-blue-100 text-blue-700 border-blue-200',
|
|
556
1423
|
},
|
|
557
|
-
|
|
1424
|
+
ongoing: {
|
|
558
1425
|
label: tClasses('status.em_andamento'),
|
|
559
1426
|
color: 'bg-emerald-100 text-emerald-700 border-emerald-200',
|
|
560
1427
|
},
|
|
561
|
-
|
|
1428
|
+
completed: {
|
|
562
1429
|
label: tClasses('status.concluida'),
|
|
563
1430
|
color: 'bg-gray-100 text-gray-700 border-gray-200',
|
|
564
1431
|
},
|
|
565
|
-
|
|
1432
|
+
cancelled: {
|
|
566
1433
|
label: tClasses('status.cancelada'),
|
|
567
1434
|
color: 'bg-red-100 text-red-700 border-red-200',
|
|
568
1435
|
},
|
|
569
1436
|
} as const;
|
|
570
|
-
const turmaStatus =
|
|
571
|
-
|
|
1437
|
+
const turmaStatus = turma?.status ?? 'open';
|
|
1438
|
+
const statusInfo = STATUS_MAP[turmaStatus] ?? { label: '', color: '' };
|
|
572
1439
|
|
|
573
1440
|
const fadeUp = {
|
|
574
1441
|
hidden: { opacity: 0, y: 20 },
|
|
575
1442
|
visible: { opacity: 1, y: 0 },
|
|
576
1443
|
};
|
|
577
1444
|
|
|
1445
|
+
const classTitle = turma?.courseTitle ?? turma?.curso ?? '';
|
|
1446
|
+
const classCode = turma?.code ?? turma?.codigo ?? '';
|
|
1447
|
+
const courseId = turma?.courseId ?? turma?.cursoId;
|
|
1448
|
+
const startDate = turma?.startDate ?? turma?.dataInicio;
|
|
1449
|
+
const endDate = turma?.endDate ?? turma?.dataFim;
|
|
1450
|
+
const schedule =
|
|
1451
|
+
turma?.startTime && turma?.endTime
|
|
1452
|
+
? `${turma.startTime} - ${turma.endTime}`
|
|
1453
|
+
: (turma?.horario ?? '—');
|
|
1454
|
+
const deliveryMode = turma?.deliveryMode ?? turma?.tipo ?? 'online';
|
|
1455
|
+
const deliveryTypeKey =
|
|
1456
|
+
deliveryMode === 'presential' ? 'presencial' : deliveryMode;
|
|
1457
|
+
const isOnline = deliveryMode === 'online' || deliveryMode === 'hybrid';
|
|
1458
|
+
const roomUrl = turma?.virtualRoomUrl ?? turma?.local ?? '';
|
|
1459
|
+
const location = turma?.location ?? turma?.local ?? '—';
|
|
1460
|
+
|
|
1461
|
+
const handleViewCourse = (): void => {
|
|
1462
|
+
if (!courseId) {
|
|
1463
|
+
toast.error('Nao foi possivel localizar o curso desta turma.');
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
router.push(`/lms/courses/${courseId}`);
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
const handleNewLesson = (): void => {
|
|
1471
|
+
openAulaSheet();
|
|
1472
|
+
};
|
|
1473
|
+
|
|
578
1474
|
return (
|
|
579
1475
|
<Page>
|
|
580
1476
|
<PageHeader
|
|
581
|
-
title={
|
|
1477
|
+
title={classTitle}
|
|
582
1478
|
breadcrumbs={[
|
|
583
1479
|
{
|
|
584
1480
|
label: t('breadcrumbs.home'),
|
|
@@ -589,21 +1485,79 @@ export default function TurmaDetalhePage() {
|
|
|
589
1485
|
href: '/lms/classes',
|
|
590
1486
|
},
|
|
591
1487
|
{
|
|
592
|
-
label: t('breadcrumbs.
|
|
1488
|
+
label: t('breadcrumbs.managementMobile'),
|
|
593
1489
|
},
|
|
594
1490
|
]}
|
|
595
|
-
actions={
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1491
|
+
actions={[
|
|
1492
|
+
{
|
|
1493
|
+
label: t('actions.viewCourse'),
|
|
1494
|
+
onClick: () => handleViewCourse(),
|
|
1495
|
+
variant: 'outline',
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
label: t('actions.newLesson'),
|
|
1499
|
+
onClick: () => handleNewLesson(),
|
|
1500
|
+
variant: 'outline',
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
label: 'Editar turma',
|
|
1504
|
+
onClick: () => setEditSheetOpen(true),
|
|
1505
|
+
variant: 'default',
|
|
1506
|
+
},
|
|
1507
|
+
]}
|
|
1508
|
+
extraContent={
|
|
1509
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
1510
|
+
<Badge className={cn('border text-[11px] font-medium')}>
|
|
1511
|
+
{classCode}
|
|
1512
|
+
<CopyButton value={classCode} className="ml-1 h-4 w-4" />
|
|
1513
|
+
</Badge>
|
|
1514
|
+
<Badge
|
|
1515
|
+
className={cn(statusInfo.color, 'border text-[11px] font-medium')}
|
|
1516
|
+
>
|
|
1517
|
+
{statusInfo.label}
|
|
1518
|
+
<CopyButton value={statusInfo.label} className="ml-1 h-4 w-4" />
|
|
1519
|
+
</Badge>
|
|
1520
|
+
<Badge variant="outline" className="gap-1 text-[11px]">
|
|
1521
|
+
{isOnline ? (
|
|
1522
|
+
<Video className="h-3 w-3" />
|
|
1523
|
+
) : (
|
|
1524
|
+
<MapPin className="h-3 w-3" />
|
|
1525
|
+
)}
|
|
1526
|
+
{tClasses(`type.${deliveryTypeKey}`)}
|
|
1527
|
+
<CopyButton
|
|
1528
|
+
value={tClasses(`type.${deliveryTypeKey}`)}
|
|
1529
|
+
className="ml-1 h-4 w-4"
|
|
1530
|
+
/>
|
|
1531
|
+
</Badge>
|
|
1532
|
+
{schedule !== '—' && (
|
|
1533
|
+
<Badge variant="outline" className="gap-1 text-[11px]">
|
|
1534
|
+
<Clock className="h-3 w-3" />
|
|
1535
|
+
{schedule}
|
|
1536
|
+
<CopyButton value={schedule} className="ml-1 h-4 w-4" />
|
|
1537
|
+
</Badge>
|
|
1538
|
+
)}
|
|
1539
|
+
{startDate && endDate && (
|
|
1540
|
+
<Badge variant="outline" className="gap-1 text-[11px]">
|
|
1541
|
+
<CalendarIcon className="h-3 w-3" />
|
|
1542
|
+
{format(new Date(startDate), 'dd/MM/yy')}
|
|
1543
|
+
{' – '}
|
|
1544
|
+
{format(new Date(endDate), 'dd/MM/yy')}
|
|
1545
|
+
<CopyButton
|
|
1546
|
+
value={`${format(new Date(startDate), 'dd/MM/yy')} – ${format(new Date(endDate), 'dd/MM/yy')}`}
|
|
1547
|
+
className="ml-1 h-4 w-4"
|
|
1548
|
+
/>
|
|
1549
|
+
</Badge>
|
|
1550
|
+
)}
|
|
1551
|
+
{primaryInstructor !== 'Nao definido' && (
|
|
1552
|
+
<Badge variant="outline" className="gap-1 text-[11px]">
|
|
1553
|
+
<Users className="h-3 w-3" />
|
|
1554
|
+
{primaryInstructor}
|
|
1555
|
+
<CopyButton
|
|
1556
|
+
value={primaryInstructor}
|
|
1557
|
+
className="ml-1 h-4 w-4"
|
|
1558
|
+
/>
|
|
1559
|
+
</Badge>
|
|
1560
|
+
)}
|
|
607
1561
|
</div>
|
|
608
1562
|
}
|
|
609
1563
|
/>
|
|
@@ -613,557 +1567,1373 @@ export default function TurmaDetalhePage() {
|
|
|
613
1567
|
initial="hidden"
|
|
614
1568
|
animate="visible"
|
|
615
1569
|
variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
|
|
1570
|
+
className="space-y-4"
|
|
616
1571
|
>
|
|
617
|
-
{/*
|
|
618
|
-
<motion.div
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1572
|
+
{/* ── KPIs ───────────────────────────────────────────────────────── */}
|
|
1573
|
+
<motion.div variants={fadeUp}>
|
|
1574
|
+
{loading ? (
|
|
1575
|
+
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-4">
|
|
1576
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
1577
|
+
<Card
|
|
1578
|
+
key={i}
|
|
1579
|
+
className="overflow-hidden border-border/70 py-0"
|
|
1580
|
+
>
|
|
1581
|
+
<div className="h-1 w-full bg-gradient-to-r from-slate-300/70 via-slate-200 to-transparent" />
|
|
1582
|
+
<CardContent className="p-4">
|
|
1583
|
+
<Skeleton className="mb-2 h-8 w-16" />
|
|
1584
|
+
<Skeleton className="h-4 w-28" />
|
|
1585
|
+
</CardContent>
|
|
1586
|
+
</Card>
|
|
1587
|
+
))}
|
|
627
1588
|
</div>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
</code>
|
|
632
|
-
<span className="mx-2">|</span>
|
|
633
|
-
{turma.professor}
|
|
634
|
-
</p>
|
|
635
|
-
</div>
|
|
1589
|
+
) : (
|
|
1590
|
+
<KpiCardsGrid items={kpis} columns={4} />
|
|
1591
|
+
)}
|
|
636
1592
|
</motion.div>
|
|
637
1593
|
|
|
638
|
-
{/*
|
|
1594
|
+
{/* ── Main layout: tabs (left) + sidebar (right) ─────────────────── */}
|
|
639
1595
|
<motion.div
|
|
640
1596
|
variants={fadeUp}
|
|
641
|
-
className="
|
|
1597
|
+
className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_300px]"
|
|
642
1598
|
>
|
|
643
|
-
{
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1599
|
+
{/* ── Left: Tabs ─────────────────────────────────────────────── */}
|
|
1600
|
+
<div className="min-w-0">
|
|
1601
|
+
<Tabs
|
|
1602
|
+
value={activeTab}
|
|
1603
|
+
onValueChange={setActiveTab}
|
|
1604
|
+
className="w-full"
|
|
1605
|
+
>
|
|
1606
|
+
<TabsList className="mb-4 h-auto grid w-full grid-cols-3 rounded-lg bg-muted/80 p-1">
|
|
1607
|
+
<TabsTrigger value="alunos" className="gap-2">
|
|
1608
|
+
<Users className="size-4" />
|
|
1609
|
+
{t('tabs.students')}
|
|
1610
|
+
{!loading && alunos.length > 0 && (
|
|
1611
|
+
<span className="ml-1 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary">
|
|
1612
|
+
{alunos.length}
|
|
1613
|
+
</span>
|
|
1614
|
+
)}
|
|
1615
|
+
</TabsTrigger>
|
|
1616
|
+
<TabsTrigger value="calendario" className="gap-2">
|
|
1617
|
+
<CalendarIcon className="size-4" />
|
|
1618
|
+
{t('tabs.calendar')}
|
|
1619
|
+
{!loading && totalSessionsCount > 0 && (
|
|
1620
|
+
<span className="ml-1 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary">
|
|
1621
|
+
{totalSessionsCount}
|
|
1622
|
+
</span>
|
|
1623
|
+
)}
|
|
1624
|
+
</TabsTrigger>
|
|
1625
|
+
<TabsTrigger value="presenca" className="gap-2">
|
|
1626
|
+
<CheckCircle2 className="size-4" />
|
|
1627
|
+
{t('tabs.attendance')}
|
|
1628
|
+
{!loading && completedSessionsCount > 0 && (
|
|
1629
|
+
<span className="ml-1 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary">
|
|
1630
|
+
{completedSessionsCount}
|
|
1631
|
+
</span>
|
|
1632
|
+
)}
|
|
1633
|
+
</TabsTrigger>
|
|
1634
|
+
</TabsList>
|
|
1635
|
+
|
|
1636
|
+
{/* ── Tab Alunos ───────────────────────────────────────────── */}
|
|
1637
|
+
<TabsContent value="alunos" className="mt-0">
|
|
1638
|
+
{/* Actions bar */}
|
|
1639
|
+
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
1640
|
+
<div className="relative flex-1">
|
|
1641
|
+
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
1642
|
+
<Input
|
|
1643
|
+
placeholder={t('students.searchPlaceholder')}
|
|
1644
|
+
value={alunoSearch}
|
|
1645
|
+
onChange={(e) => setAlunoSearch(e.target.value)}
|
|
1646
|
+
className="pl-9"
|
|
1647
|
+
/>
|
|
1648
|
+
</div>
|
|
1649
|
+
<div className="flex items-center gap-2">
|
|
1650
|
+
<Popover
|
|
1651
|
+
open={studentPickerOpen}
|
|
1652
|
+
onOpenChange={(open) => {
|
|
1653
|
+
setStudentPickerOpen(open);
|
|
1654
|
+
if (!open) setPessoaSearch('');
|
|
1655
|
+
}}
|
|
1656
|
+
>
|
|
1657
|
+
<PopoverTrigger asChild>
|
|
1658
|
+
<Button
|
|
1659
|
+
type="button"
|
|
1660
|
+
variant="outline"
|
|
1661
|
+
className="gap-2"
|
|
1662
|
+
>
|
|
1663
|
+
<UserPlus className="size-4" />
|
|
1664
|
+
{t('students.actions.addStudent')}
|
|
1665
|
+
{searchingPessoas && (
|
|
1666
|
+
<Loader2 className="size-4 animate-spin" />
|
|
1667
|
+
)}
|
|
1668
|
+
</Button>
|
|
1669
|
+
</PopoverTrigger>
|
|
1670
|
+
<PopoverContent className="w-80 p-0" align="end">
|
|
1671
|
+
<Command shouldFilter={false}>
|
|
1672
|
+
<CommandInput
|
|
1673
|
+
placeholder={t(
|
|
1674
|
+
'dialogs.addStudents.searchPlaceholder'
|
|
1675
|
+
)}
|
|
1676
|
+
value={pessoaSearch}
|
|
1677
|
+
onValueChange={setPessoaSearch}
|
|
1678
|
+
/>
|
|
1679
|
+
<CommandList>
|
|
1680
|
+
<CommandEmpty>
|
|
1681
|
+
<div className="px-3 py-4 text-sm text-muted-foreground">
|
|
1682
|
+
{t('dialogs.addStudents.notFound')}
|
|
1683
|
+
</div>
|
|
1684
|
+
</CommandEmpty>
|
|
1685
|
+
<CommandGroup>
|
|
1686
|
+
{(pessoaSearch
|
|
1687
|
+
? pessoasElegiveisParaMatricula
|
|
1688
|
+
: studentPickerResults.filter((person) => {
|
|
1689
|
+
const isInstructor = Boolean(
|
|
1690
|
+
person.isInstructor
|
|
1691
|
+
);
|
|
1692
|
+
const canTeachCourses = Boolean(
|
|
1693
|
+
person.canTeachCourses
|
|
1694
|
+
);
|
|
1695
|
+
const isStudentByEnrollment = Boolean(
|
|
1696
|
+
person.isStudentByEnrollment
|
|
1697
|
+
);
|
|
1698
|
+
return !(
|
|
1699
|
+
isInstructor &&
|
|
1700
|
+
canTeachCourses &&
|
|
1701
|
+
!isStudentByEnrollment
|
|
1702
|
+
);
|
|
1703
|
+
})
|
|
1704
|
+
).map((pessoa) => {
|
|
1705
|
+
const isSelected = selectedPessoas.includes(
|
|
1706
|
+
pessoa.id
|
|
1707
|
+
);
|
|
1708
|
+
return (
|
|
1709
|
+
<CommandItem
|
|
1710
|
+
key={pessoa.id}
|
|
1711
|
+
value={`${pessoa.nome}-${pessoa.id}`}
|
|
1712
|
+
onSelect={() => {
|
|
1713
|
+
setSelectedPessoas((prev) =>
|
|
1714
|
+
prev.includes(pessoa.id)
|
|
1715
|
+
? prev.filter(
|
|
1716
|
+
(pid) => pid !== pessoa.id
|
|
1717
|
+
)
|
|
1718
|
+
: [...prev, pessoa.id]
|
|
1719
|
+
);
|
|
1720
|
+
}}
|
|
1721
|
+
>
|
|
1722
|
+
<div className="flex w-full items-center gap-3">
|
|
1723
|
+
<Checkbox checked={isSelected} />
|
|
1724
|
+
<Avatar className="size-8">
|
|
1725
|
+
<AvatarFallback className="text-[10px]">
|
|
1726
|
+
{pessoa.nome
|
|
1727
|
+
.split(' ')
|
|
1728
|
+
.map((n: string) => n[0])
|
|
1729
|
+
.join('')
|
|
1730
|
+
.slice(0, 2)}
|
|
1731
|
+
</AvatarFallback>
|
|
1732
|
+
</Avatar>
|
|
1733
|
+
<div className="min-w-0 flex-1">
|
|
1734
|
+
<p className="truncate text-sm font-medium">
|
|
1735
|
+
{pessoa.nome}
|
|
1736
|
+
</p>
|
|
1737
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1738
|
+
{pessoa.email}
|
|
1739
|
+
</p>
|
|
1740
|
+
</div>
|
|
1741
|
+
{isSelected && (
|
|
1742
|
+
<Check className="size-4 shrink-0 text-primary" />
|
|
1743
|
+
)}
|
|
1744
|
+
</div>
|
|
1745
|
+
</CommandItem>
|
|
1746
|
+
);
|
|
1747
|
+
})}
|
|
1748
|
+
</CommandGroup>
|
|
1749
|
+
</CommandList>
|
|
1750
|
+
</Command>
|
|
1751
|
+
</PopoverContent>
|
|
1752
|
+
</Popover>
|
|
1753
|
+
<Button
|
|
1754
|
+
type="button"
|
|
1755
|
+
variant="outline"
|
|
1756
|
+
size="icon"
|
|
1757
|
+
className="h-9 w-9 shrink-0 cursor-pointer"
|
|
1758
|
+
onClick={openCreateStudentDialog}
|
|
1759
|
+
aria-label={t('dialogs.addStudents.createNew')}
|
|
1760
|
+
>
|
|
1761
|
+
<Plus className="size-4" />
|
|
1762
|
+
</Button>
|
|
1763
|
+
</div>
|
|
1764
|
+
</div>
|
|
1765
|
+
|
|
1766
|
+
{/* Pending enrollment banner */}
|
|
1767
|
+
{selectedPessoas.length > 0 && (
|
|
1768
|
+
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-primary/20 bg-primary/5 px-4 py-2.5 text-sm">
|
|
1769
|
+
<span className="font-medium">
|
|
1770
|
+
{t('dialogs.addStudents.confirm')} (
|
|
1771
|
+
{selectedPessoas.length})
|
|
1772
|
+
</span>
|
|
1773
|
+
<Button
|
|
1774
|
+
size="sm"
|
|
1775
|
+
className="gap-2"
|
|
1776
|
+
onClick={handleAddAlunos}
|
|
1777
|
+
disabled={savingStudents}
|
|
1778
|
+
>
|
|
1779
|
+
{savingStudents && (
|
|
1780
|
+
<Loader2 className="size-4 animate-spin" />
|
|
1781
|
+
)}
|
|
1782
|
+
<UserPlus className="size-4" />
|
|
1783
|
+
{t('students.actions.addStudent')}
|
|
1784
|
+
</Button>
|
|
1785
|
+
<Button
|
|
1786
|
+
variant="ghost"
|
|
1787
|
+
size="sm"
|
|
1788
|
+
onClick={() => {
|
|
1789
|
+
setSelectedPessoas([]);
|
|
1790
|
+
setPessoaSearch('');
|
|
1791
|
+
setStudentPickerOpen(false);
|
|
1792
|
+
setStudentPickerResults([]);
|
|
1793
|
+
}}
|
|
1794
|
+
>
|
|
1795
|
+
{t('students.actions.clearSelection')}
|
|
1796
|
+
</Button>
|
|
1797
|
+
</div>
|
|
1798
|
+
)}
|
|
1799
|
+
|
|
1800
|
+
{/* Bulk selection banner */}
|
|
1801
|
+
{selectedAlunos.length > 0 && (
|
|
1802
|
+
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2.5 text-sm dark:border-amber-800 dark:bg-amber-950/30">
|
|
1803
|
+
<Checkbox
|
|
1804
|
+
checked={
|
|
1805
|
+
selectedAlunos.length === filteredAlunos.length
|
|
1806
|
+
}
|
|
1807
|
+
onCheckedChange={(checked) =>
|
|
1808
|
+
setSelectedAlunos(
|
|
1809
|
+
checked ? filteredAlunos.map((a) => a.id) : []
|
|
1810
|
+
)
|
|
1811
|
+
}
|
|
1812
|
+
/>
|
|
1813
|
+
<span className="font-medium">
|
|
1814
|
+
{t('students.selectedCount', {
|
|
1815
|
+
count: selectedAlunos.length,
|
|
1816
|
+
})}
|
|
1817
|
+
</span>
|
|
1818
|
+
<Button
|
|
1819
|
+
variant="destructive"
|
|
1820
|
+
size="sm"
|
|
1821
|
+
className="ml-auto gap-2"
|
|
1822
|
+
onClick={handleRemoveSelectedAlunos}
|
|
1823
|
+
>
|
|
1824
|
+
<UserMinus className="size-4" />
|
|
1825
|
+
{t('students.actions.removeSelected', {
|
|
1826
|
+
count: selectedAlunos.length,
|
|
1827
|
+
})}
|
|
1828
|
+
</Button>
|
|
1829
|
+
<Button
|
|
1830
|
+
variant="ghost"
|
|
1831
|
+
size="sm"
|
|
1832
|
+
onClick={() => setSelectedAlunos([])}
|
|
1833
|
+
>
|
|
1834
|
+
{t('students.actions.clearSelection')}
|
|
1835
|
+
</Button>
|
|
1836
|
+
</div>
|
|
1837
|
+
)}
|
|
1838
|
+
|
|
1839
|
+
{/* Student list */}
|
|
1840
|
+
{loading ? (
|
|
1841
|
+
<div className="space-y-2">
|
|
1842
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
1843
|
+
<div
|
|
1844
|
+
key={i}
|
|
1845
|
+
className="flex items-center gap-3 rounded-lg border border-border/70 p-3"
|
|
1846
|
+
>
|
|
1847
|
+
<Skeleton className="h-9 w-9 rounded-full" />
|
|
1848
|
+
<div className="flex-1 space-y-1">
|
|
1849
|
+
<Skeleton className="h-4 w-36" />
|
|
1850
|
+
<Skeleton className="h-3 w-48" />
|
|
1851
|
+
</div>
|
|
1852
|
+
<Skeleton className="h-4 w-12" />
|
|
1853
|
+
</div>
|
|
1854
|
+
))}
|
|
1855
|
+
</div>
|
|
1856
|
+
) : filteredAlunos.length === 0 ? (
|
|
1857
|
+
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 px-6 py-10 text-center">
|
|
1858
|
+
<div className="flex flex-col items-center gap-3">
|
|
1859
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted/60 text-muted-foreground">
|
|
1860
|
+
<Users className="size-5" />
|
|
1861
|
+
</div>
|
|
661
1862
|
<div>
|
|
662
|
-
<p className="
|
|
663
|
-
{
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
{kpi.valor}
|
|
1863
|
+
<p className="font-semibold text-sm">
|
|
1864
|
+
{alunoSearch
|
|
1865
|
+
? t('students.empty.notFound')
|
|
1866
|
+
: t('students.empty.notEnrolled')}
|
|
667
1867
|
</p>
|
|
668
1868
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
669
|
-
{
|
|
1869
|
+
{alunoSearch
|
|
1870
|
+
? t('students.empty.notFoundDescription')
|
|
1871
|
+
: t('students.empty.notEnrolledDescription')}
|
|
670
1872
|
</p>
|
|
671
1873
|
</div>
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1874
|
+
{alunoSearch ? (
|
|
1875
|
+
<Button
|
|
1876
|
+
size="sm"
|
|
1877
|
+
variant="outline"
|
|
1878
|
+
onClick={() => setAlunoSearch('')}
|
|
1879
|
+
>
|
|
1880
|
+
{t('students.empty.clearSearch')}
|
|
1881
|
+
</Button>
|
|
1882
|
+
) : (
|
|
1883
|
+
<div className="flex flex-wrap justify-center gap-2">
|
|
1884
|
+
<Button
|
|
1885
|
+
size="sm"
|
|
1886
|
+
variant="outline"
|
|
1887
|
+
className="gap-2"
|
|
1888
|
+
onClick={() => setStudentPickerOpen(true)}
|
|
1889
|
+
>
|
|
1890
|
+
<Search className="size-3.5" />
|
|
1891
|
+
{t('students.actions.addStudent')}
|
|
1892
|
+
</Button>
|
|
1893
|
+
<Button
|
|
1894
|
+
size="sm"
|
|
1895
|
+
className="gap-2"
|
|
1896
|
+
onClick={openCreateStudentDialog}
|
|
1897
|
+
>
|
|
1898
|
+
<Plus className="size-3.5" />
|
|
1899
|
+
{t('dialogs.addStudents.createNew')}
|
|
1900
|
+
</Button>
|
|
1901
|
+
</div>
|
|
1902
|
+
)}
|
|
1903
|
+
</div>
|
|
1904
|
+
</div>
|
|
1905
|
+
) : (
|
|
1906
|
+
<div className="space-y-1.5">
|
|
1907
|
+
{filteredAlunos.map((aluno) => {
|
|
1908
|
+
const isSelected = selectedAlunos.includes(aluno.id);
|
|
1909
|
+
return (
|
|
1910
|
+
<div
|
|
1911
|
+
key={aluno.id}
|
|
1912
|
+
className={cn(
|
|
1913
|
+
'group flex cursor-pointer items-center gap-3 rounded-lg border border-border/70 px-3 py-2.5 transition-colors hover:bg-muted/50',
|
|
1914
|
+
isSelected && 'border-primary/40 bg-primary/5'
|
|
1915
|
+
)}
|
|
1916
|
+
onClick={(e) => toggleSelectAluno(aluno.id, e)}
|
|
1917
|
+
>
|
|
1918
|
+
<Checkbox
|
|
1919
|
+
checked={isSelected}
|
|
1920
|
+
onClick={(e) => e.stopPropagation()}
|
|
1921
|
+
onCheckedChange={() =>
|
|
1922
|
+
toggleSelectAluno(aluno.id)
|
|
1923
|
+
}
|
|
1924
|
+
className="shrink-0"
|
|
1925
|
+
/>
|
|
1926
|
+
<Avatar className="size-8 shrink-0">
|
|
1927
|
+
<AvatarFallback className="bg-gradient-to-br from-blue-100 to-blue-200 text-[11px] font-medium text-blue-700">
|
|
1928
|
+
{getPersonInitials(aluno.nome)}
|
|
1929
|
+
</AvatarFallback>
|
|
1930
|
+
</Avatar>
|
|
1931
|
+
<div className="min-w-0 flex-1">
|
|
1932
|
+
<p className="truncate text-sm font-medium">
|
|
1933
|
+
{aluno.nome}
|
|
1934
|
+
</p>
|
|
1935
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1936
|
+
{aluno.email}
|
|
1937
|
+
</p>
|
|
1938
|
+
</div>
|
|
1939
|
+
<div className="hidden shrink-0 items-center gap-3 sm:flex">
|
|
1940
|
+
<div className="w-16 text-right">
|
|
1941
|
+
<p className="text-[10px] text-muted-foreground">
|
|
1942
|
+
{aluno.progresso}%
|
|
1943
|
+
</p>
|
|
1944
|
+
<div className="mt-0.5 h-1 w-full overflow-hidden rounded-full bg-muted">
|
|
1945
|
+
<div
|
|
1946
|
+
className="h-full rounded-full bg-emerald-500"
|
|
1947
|
+
style={{ width: `${aluno.progresso}%` }}
|
|
1948
|
+
/>
|
|
1949
|
+
</div>
|
|
1950
|
+
</div>
|
|
1951
|
+
<Badge variant="outline" className="text-[10px]">
|
|
1952
|
+
{aluno.status}
|
|
1953
|
+
</Badge>
|
|
1954
|
+
</div>
|
|
1955
|
+
<DropdownMenu>
|
|
1956
|
+
<DropdownMenuTrigger asChild>
|
|
1957
|
+
<Button
|
|
1958
|
+
variant="ghost"
|
|
1959
|
+
size="icon"
|
|
1960
|
+
className="size-8 shrink-0 opacity-0 group-hover:opacity-100"
|
|
1961
|
+
onClick={(e) => e.stopPropagation()}
|
|
1962
|
+
>
|
|
1963
|
+
<MoreHorizontal className="size-4" />
|
|
1964
|
+
</Button>
|
|
1965
|
+
</DropdownMenuTrigger>
|
|
1966
|
+
<DropdownMenuContent align="end">
|
|
1967
|
+
<DropdownMenuItem
|
|
1968
|
+
onClick={(e) => {
|
|
1969
|
+
e.stopPropagation();
|
|
1970
|
+
void openStudentProfile(aluno.id);
|
|
1971
|
+
}}
|
|
1972
|
+
>
|
|
1973
|
+
<Eye className="mr-2 size-4" />
|
|
1974
|
+
{t('students.menu.viewProfile')}
|
|
1975
|
+
</DropdownMenuItem>
|
|
1976
|
+
<DropdownMenuSeparator />
|
|
1977
|
+
<DropdownMenuItem
|
|
1978
|
+
className="text-destructive"
|
|
1979
|
+
onClick={(e) => {
|
|
1980
|
+
e.stopPropagation();
|
|
1981
|
+
setAlunoToRemove(aluno);
|
|
1982
|
+
setRemoveAlunoDialogOpen(true);
|
|
1983
|
+
}}
|
|
1984
|
+
>
|
|
1985
|
+
<UserMinus className="mr-2 size-4" />
|
|
1986
|
+
{t('students.menu.removeFromClass')}
|
|
1987
|
+
</DropdownMenuItem>
|
|
1988
|
+
</DropdownMenuContent>
|
|
1989
|
+
</DropdownMenu>
|
|
1990
|
+
</div>
|
|
1991
|
+
);
|
|
1992
|
+
})}
|
|
1993
|
+
</div>
|
|
1994
|
+
)}
|
|
1995
|
+
</TabsContent>
|
|
1996
|
+
|
|
1997
|
+
{/* ── Tab Calendario ──────────────────────────────────────────── */}
|
|
1998
|
+
<TabsContent value="calendario" className="mt-0">
|
|
1999
|
+
{/* ── View mode toolbar ─────────────────────────────────────── */}
|
|
2000
|
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
2001
|
+
<div className="flex items-center rounded-lg border border-border/60 bg-muted/40 p-0.5">
|
|
2002
|
+
{(['single', 'quarter', 'year', 'list'] as const).map(
|
|
2003
|
+
(mode) => (
|
|
2004
|
+
<button
|
|
2005
|
+
key={mode}
|
|
2006
|
+
type="button"
|
|
2007
|
+
onClick={() => setCalendarViewMode(mode)}
|
|
2008
|
+
className={cn(
|
|
2009
|
+
'rounded-md px-3 py-1 text-xs font-medium transition-colors',
|
|
2010
|
+
calendarViewMode === mode
|
|
2011
|
+
? 'bg-background text-foreground shadow-sm'
|
|
2012
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
2013
|
+
)}
|
|
2014
|
+
>
|
|
2015
|
+
{mode === 'single'
|
|
2016
|
+
? t('calendar.viewSingle')
|
|
2017
|
+
: mode === 'quarter'
|
|
2018
|
+
? t('calendar.viewQuarter')
|
|
2019
|
+
: mode === 'year'
|
|
2020
|
+
? t('calendar.viewYear')
|
|
2021
|
+
: 'Lista'}
|
|
2022
|
+
</button>
|
|
2023
|
+
)
|
|
2024
|
+
)}
|
|
2025
|
+
</div>
|
|
2026
|
+
{calendarViewMode !== 'single' &&
|
|
2027
|
+
calendarViewMode !== 'list' && (
|
|
2028
|
+
<div className="flex items-center gap-1">
|
|
2029
|
+
<Button
|
|
2030
|
+
variant="ghost"
|
|
2031
|
+
size="icon"
|
|
2032
|
+
className="size-7"
|
|
2033
|
+
onClick={() =>
|
|
2034
|
+
setCalendarViewDate((d) =>
|
|
2035
|
+
addMonths(
|
|
2036
|
+
d,
|
|
2037
|
+
calendarViewMode === 'year' ? -12 : -3
|
|
2038
|
+
)
|
|
2039
|
+
)
|
|
2040
|
+
}
|
|
2041
|
+
>
|
|
2042
|
+
<ChevronLeft className="size-3.5" />
|
|
2043
|
+
</Button>
|
|
2044
|
+
<span className="min-w-[130px] text-center text-sm font-medium">
|
|
2045
|
+
{calendarViewMode === 'year'
|
|
2046
|
+
? format(calendarViewDate, 'yyyy')
|
|
2047
|
+
: `${format(calendarViewDate, 'MMM', { locale: dateLocale })} – ${format(addMonths(calendarViewDate, 2), 'MMM yyyy', { locale: dateLocale })}`}
|
|
2048
|
+
</span>
|
|
2049
|
+
<Button
|
|
2050
|
+
variant="ghost"
|
|
2051
|
+
size="icon"
|
|
2052
|
+
className="size-7"
|
|
2053
|
+
onClick={() =>
|
|
2054
|
+
setCalendarViewDate((d) =>
|
|
2055
|
+
addMonths(
|
|
2056
|
+
d,
|
|
2057
|
+
calendarViewMode === 'year' ? 12 : 3
|
|
2058
|
+
)
|
|
2059
|
+
)
|
|
2060
|
+
}
|
|
2061
|
+
>
|
|
2062
|
+
<ChevronRight className="size-3.5" />
|
|
2063
|
+
</Button>
|
|
676
2064
|
</div>
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
2065
|
+
)}
|
|
2066
|
+
</div>
|
|
2067
|
+
|
|
2068
|
+
{calendarViewMode === 'single' ? (
|
|
2069
|
+
<LmsClassCalendar
|
|
2070
|
+
events={calendarEvents}
|
|
2071
|
+
classId={id}
|
|
2072
|
+
alunos={alunos}
|
|
2073
|
+
locale={locale}
|
|
2074
|
+
defaultStartTime={turma?.startTime}
|
|
2075
|
+
defaultEndTime={turma?.endTime}
|
|
2076
|
+
helperText={t('calendar.helper')}
|
|
2077
|
+
newLessonLabel={t('actions.newLesson')}
|
|
2078
|
+
calendarMessages={calendarMessages}
|
|
2079
|
+
mobileCalendarLabels={{
|
|
2080
|
+
previousLabel: t('calendar.messages.previous'),
|
|
2081
|
+
nextLabel: t('calendar.messages.next'),
|
|
2082
|
+
noEventsLabel: t('calendar.messages.noEventsInRange'),
|
|
2083
|
+
}}
|
|
2084
|
+
onNewLesson={() => openAulaSheet()}
|
|
2085
|
+
onOpenAulaSheet={openAulaSheet}
|
|
2086
|
+
onSessionSaved={handleQuickCreateSaved}
|
|
2087
|
+
/>
|
|
2088
|
+
) : calendarViewMode === 'list' ? (
|
|
2089
|
+
aulasState.length === 0 ? (
|
|
2090
|
+
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 px-6 py-10 text-center">
|
|
2091
|
+
<div className="flex flex-col items-center gap-3">
|
|
2092
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted/60 text-muted-foreground">
|
|
2093
|
+
<CalendarIcon className="size-5" />
|
|
2094
|
+
</div>
|
|
2095
|
+
<div>
|
|
2096
|
+
<p className="text-sm font-semibold">
|
|
2097
|
+
{t('calendar.empty.title')}
|
|
2098
|
+
</p>
|
|
2099
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2100
|
+
{t('calendar.empty.description')}
|
|
2101
|
+
</p>
|
|
2102
|
+
</div>
|
|
2103
|
+
<Button
|
|
2104
|
+
size="sm"
|
|
2105
|
+
variant="outline"
|
|
2106
|
+
className="gap-2"
|
|
2107
|
+
onClick={() => openAulaSheet()}
|
|
2108
|
+
>
|
|
2109
|
+
<Plus className="size-3.5" />
|
|
2110
|
+
{t('actions.newLesson')}
|
|
2111
|
+
</Button>
|
|
2112
|
+
</div>
|
|
2113
|
+
</div>
|
|
2114
|
+
) : (
|
|
2115
|
+
<div className="rounded-xl border border-border/60 overflow-hidden">
|
|
2116
|
+
<Table>
|
|
2117
|
+
<TableHeader>
|
|
2118
|
+
<TableRow className="bg-muted/40 hover:bg-muted/40">
|
|
2119
|
+
<TableHead className="w-[110px]">Data</TableHead>
|
|
2120
|
+
<TableHead className="w-[110px]">
|
|
2121
|
+
Horário
|
|
2122
|
+
</TableHead>
|
|
2123
|
+
<TableHead>Título</TableHead>
|
|
2124
|
+
<TableHead className="w-[100px]">Tipo</TableHead>
|
|
2125
|
+
<TableHead>Local / Link</TableHead>
|
|
2126
|
+
<TableHead>Instrutor</TableHead>
|
|
2127
|
+
<TableHead className="w-[60px]" />
|
|
2128
|
+
</TableRow>
|
|
2129
|
+
</TableHeader>
|
|
2130
|
+
<TableBody>
|
|
2131
|
+
{aulasState.map((aula) => {
|
|
2132
|
+
const sessionColor =
|
|
2133
|
+
aula.cor || aula.color || '#3b82f6';
|
|
2134
|
+
const isPast = getSessionEndDate(aula) < now;
|
|
2135
|
+
return (
|
|
2136
|
+
<TableRow
|
|
2137
|
+
key={aula.id}
|
|
2138
|
+
className="cursor-pointer hover:bg-muted/30"
|
|
2139
|
+
onClick={() => openAulaSheet(aula)}
|
|
2140
|
+
>
|
|
2141
|
+
<TableCell>
|
|
2142
|
+
<div className="flex items-center gap-2">
|
|
2143
|
+
<span
|
|
2144
|
+
className="inline-block h-2 w-2 shrink-0 rounded-full"
|
|
2145
|
+
style={{
|
|
2146
|
+
backgroundColor: sessionColor,
|
|
2147
|
+
}}
|
|
2148
|
+
/>
|
|
2149
|
+
<span
|
|
2150
|
+
className={cn(
|
|
2151
|
+
'text-sm tabular-nums',
|
|
2152
|
+
isPast && 'text-muted-foreground'
|
|
2153
|
+
)}
|
|
2154
|
+
>
|
|
2155
|
+
{format(
|
|
2156
|
+
parseSessionDate(aula.data),
|
|
2157
|
+
'dd/MM/yyyy',
|
|
2158
|
+
{ locale: dateLocale }
|
|
2159
|
+
)}
|
|
2160
|
+
</span>
|
|
2161
|
+
</div>
|
|
2162
|
+
</TableCell>
|
|
2163
|
+
<TableCell className="tabular-nums text-sm text-muted-foreground">
|
|
2164
|
+
{aula.horaInicio} – {aula.horaFim}
|
|
2165
|
+
</TableCell>
|
|
2166
|
+
<TableCell>
|
|
2167
|
+
<span className="text-sm font-medium">
|
|
2168
|
+
{aula.titulo}
|
|
2169
|
+
</span>
|
|
2170
|
+
</TableCell>
|
|
2171
|
+
<TableCell>
|
|
2172
|
+
<Badge
|
|
2173
|
+
variant="outline"
|
|
2174
|
+
className="gap-1 text-[11px]"
|
|
2175
|
+
>
|
|
2176
|
+
{aula.tipo === 'online' ? (
|
|
2177
|
+
<Video className="size-3" />
|
|
2178
|
+
) : (
|
|
2179
|
+
<MapPin className="size-3" />
|
|
2180
|
+
)}
|
|
2181
|
+
{tClasses(`type.${aula.tipo}`)}
|
|
2182
|
+
</Badge>
|
|
2183
|
+
</TableCell>
|
|
2184
|
+
<TableCell className="max-w-[180px] truncate text-sm text-muted-foreground">
|
|
2185
|
+
{aula.meetingUrl || aula.local || '—'}
|
|
2186
|
+
</TableCell>
|
|
2187
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
2188
|
+
{aula.instructorName || '—'}
|
|
2189
|
+
</TableCell>
|
|
2190
|
+
<TableCell>
|
|
2191
|
+
<Button
|
|
2192
|
+
variant="ghost"
|
|
2193
|
+
size="icon"
|
|
2194
|
+
className="size-7"
|
|
2195
|
+
onClick={(e) => {
|
|
2196
|
+
e.stopPropagation();
|
|
2197
|
+
openAulaSheet(aula);
|
|
2198
|
+
}}
|
|
2199
|
+
>
|
|
2200
|
+
<Pencil className="size-3.5" />
|
|
2201
|
+
</Button>
|
|
2202
|
+
</TableCell>
|
|
2203
|
+
</TableRow>
|
|
2204
|
+
);
|
|
2205
|
+
})}
|
|
2206
|
+
</TableBody>
|
|
2207
|
+
</Table>
|
|
2208
|
+
</div>
|
|
2209
|
+
)
|
|
2210
|
+
) : (
|
|
2211
|
+
<div
|
|
2212
|
+
className={cn(
|
|
2213
|
+
'grid gap-3',
|
|
2214
|
+
calendarViewMode === 'year'
|
|
2215
|
+
? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
|
|
2216
|
+
: 'grid-cols-1 sm:grid-cols-3'
|
|
2217
|
+
)}
|
|
2218
|
+
>
|
|
2219
|
+
{Array.from(
|
|
2220
|
+
{ length: calendarViewMode === 'year' ? 12 : 3 },
|
|
2221
|
+
(_, monthIdx) => {
|
|
2222
|
+
const monthBase =
|
|
2223
|
+
calendarViewMode === 'year'
|
|
2224
|
+
? new Date(calendarViewDate.getFullYear(), 0, 1)
|
|
2225
|
+
: startOfMonth(calendarViewDate);
|
|
2226
|
+
const month = addMonths(monthBase, monthIdx);
|
|
2227
|
+
const mStart = startOfMonth(month);
|
|
2228
|
+
const mEnd = endOfMonth(month);
|
|
2229
|
+
const calStart = startOfWeek(mStart, {
|
|
2230
|
+
weekStartsOn: 1,
|
|
2231
|
+
});
|
|
2232
|
+
const calEnd = endOfWeek(mEnd, { weekStartsOn: 1 });
|
|
2233
|
+
const days = eachDayOfInterval({
|
|
2234
|
+
start: calStart,
|
|
2235
|
+
end: calEnd,
|
|
2236
|
+
});
|
|
2237
|
+
const today = new Date();
|
|
2238
|
+
// Jan 1 2024 was a Monday — use as stable reference for weekday single letters
|
|
2239
|
+
const weekLetters = Array.from(
|
|
2240
|
+
{ length: 7 },
|
|
2241
|
+
(_, wi) => {
|
|
2242
|
+
const ref = new Date(2024, 0, 1 + wi);
|
|
2243
|
+
return format(ref, 'EEEEE', {
|
|
2244
|
+
locale: dateLocale,
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
);
|
|
682
2248
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
2249
|
+
return (
|
|
2250
|
+
<div
|
|
2251
|
+
key={format(month, 'yyyy-MM')}
|
|
2252
|
+
className="rounded-xl border border-border/60 bg-muted/20 p-3"
|
|
2253
|
+
>
|
|
2254
|
+
<p className="mb-2 text-center text-[11px] font-semibold capitalize text-foreground">
|
|
2255
|
+
{format(month, 'MMMM yyyy', {
|
|
2256
|
+
locale: dateLocale,
|
|
2257
|
+
})}
|
|
2258
|
+
</p>
|
|
2259
|
+
<div className="grid grid-cols-7 gap-0.5">
|
|
2260
|
+
{weekLetters.map((d, di) => (
|
|
2261
|
+
<div
|
|
2262
|
+
key={di}
|
|
2263
|
+
className="pb-1 text-center text-[9px] font-medium uppercase text-muted-foreground"
|
|
2264
|
+
>
|
|
2265
|
+
{d}
|
|
2266
|
+
</div>
|
|
2267
|
+
))}
|
|
2268
|
+
{days.map((day) => {
|
|
2269
|
+
const dayKey = format(day, 'yyyy-MM-dd');
|
|
2270
|
+
const daySessions =
|
|
2271
|
+
sessionsByDay.get(dayKey) ?? [];
|
|
2272
|
+
const isCurrentMonth = isSameMonth(
|
|
2273
|
+
day,
|
|
2274
|
+
month
|
|
2275
|
+
);
|
|
2276
|
+
const isToday = isSameDay(day, today);
|
|
2277
|
+
const hasSessions =
|
|
2278
|
+
daySessions.length > 0 && isCurrentMonth;
|
|
2279
|
+
|
|
2280
|
+
return (
|
|
2281
|
+
<button
|
|
2282
|
+
key={dayKey}
|
|
2283
|
+
type="button"
|
|
2284
|
+
className={cn(
|
|
2285
|
+
'flex flex-col items-center rounded-md py-0.5 transition-colors',
|
|
2286
|
+
isCurrentMonth
|
|
2287
|
+
? 'text-foreground'
|
|
2288
|
+
: 'text-muted-foreground/20',
|
|
2289
|
+
hasSessions
|
|
2290
|
+
? 'cursor-pointer hover:bg-muted/60'
|
|
2291
|
+
: 'cursor-default'
|
|
2292
|
+
)}
|
|
2293
|
+
onClick={() => {
|
|
2294
|
+
if (!hasSessions) return;
|
|
2295
|
+
if (daySessions.length === 1) {
|
|
2296
|
+
openAulaSheet(daySessions[0]);
|
|
2297
|
+
} else {
|
|
2298
|
+
setCalendarViewMode('single');
|
|
2299
|
+
}
|
|
2300
|
+
}}
|
|
2301
|
+
title={
|
|
2302
|
+
hasSessions
|
|
2303
|
+
? daySessions
|
|
2304
|
+
.map((a) => a.titulo)
|
|
2305
|
+
.join(', ')
|
|
2306
|
+
: undefined
|
|
2307
|
+
}
|
|
2308
|
+
>
|
|
2309
|
+
<span
|
|
2310
|
+
className={cn(
|
|
2311
|
+
'flex h-5 w-5 items-center justify-center rounded-full text-[10px]',
|
|
2312
|
+
isToday &&
|
|
2313
|
+
'bg-primary font-bold text-primary-foreground'
|
|
2314
|
+
)}
|
|
2315
|
+
>
|
|
2316
|
+
{format(day, 'd')}
|
|
2317
|
+
</span>
|
|
2318
|
+
<div className="flex h-1.5 items-center justify-center gap-0.5">
|
|
2319
|
+
{hasSessions &&
|
|
2320
|
+
daySessions
|
|
2321
|
+
.slice(0, 3)
|
|
2322
|
+
.map((aula, ai) => (
|
|
2323
|
+
<span
|
|
2324
|
+
key={ai}
|
|
2325
|
+
className="h-1 w-1 rounded-full"
|
|
2326
|
+
style={{
|
|
2327
|
+
backgroundColor:
|
|
2328
|
+
aula.cor ||
|
|
2329
|
+
aula.color ||
|
|
2330
|
+
'#3b82f6',
|
|
2331
|
+
}}
|
|
2332
|
+
/>
|
|
2333
|
+
))}
|
|
2334
|
+
</div>
|
|
2335
|
+
</button>
|
|
2336
|
+
);
|
|
2337
|
+
})}
|
|
2338
|
+
</div>
|
|
2339
|
+
</div>
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
)}
|
|
691
2343
|
</div>
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
2344
|
+
)}
|
|
2345
|
+
</TabsContent>
|
|
2346
|
+
|
|
2347
|
+
{/* ── Tab Presenca ────────────────────────────────────────────── */}
|
|
2348
|
+
<TabsContent value="presenca" className="mt-0">
|
|
2349
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
2350
|
+
{t('attendance.helper')}
|
|
2351
|
+
</p>
|
|
2352
|
+
{loading ? (
|
|
2353
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
2354
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
2355
|
+
<Card
|
|
2356
|
+
key={i}
|
|
2357
|
+
className="overflow-hidden border-border/70"
|
|
2358
|
+
>
|
|
2359
|
+
<CardContent className="p-4">
|
|
2360
|
+
<Skeleton className="h-20" />
|
|
2361
|
+
</CardContent>
|
|
2362
|
+
</Card>
|
|
2363
|
+
))}
|
|
700
2364
|
</div>
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
2365
|
+
) : aulasState.length === 0 ? (
|
|
2366
|
+
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 px-6 py-10 text-center">
|
|
2367
|
+
<div className="flex flex-col items-center gap-3">
|
|
2368
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted/60 text-muted-foreground">
|
|
2369
|
+
<CheckCircle2 className="size-5" />
|
|
2370
|
+
</div>
|
|
2371
|
+
<div>
|
|
2372
|
+
<p className="font-semibold text-sm">
|
|
2373
|
+
{t('attendance.empty.title')}
|
|
2374
|
+
</p>
|
|
2375
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
2376
|
+
{t('attendance.empty.description')}
|
|
2377
|
+
</p>
|
|
2378
|
+
</div>
|
|
2379
|
+
<Button
|
|
2380
|
+
size="sm"
|
|
2381
|
+
variant="outline"
|
|
2382
|
+
className="gap-2"
|
|
2383
|
+
onClick={() => {
|
|
2384
|
+
openAulaSheet();
|
|
2385
|
+
setActiveTab('calendario');
|
|
2386
|
+
}}
|
|
2387
|
+
>
|
|
2388
|
+
<Plus className="size-3.5" />
|
|
2389
|
+
{t('actions.newLesson')}
|
|
2390
|
+
</Button>
|
|
2391
|
+
</div>
|
|
705
2392
|
</div>
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
2393
|
+
) : (
|
|
2394
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
2395
|
+
{aulasState
|
|
2396
|
+
.slice(-12)
|
|
2397
|
+
.reverse()
|
|
2398
|
+
.map((aula) => {
|
|
2399
|
+
const sessionColor =
|
|
2400
|
+
aula.cor || aula.color || '#3b82f6';
|
|
2401
|
+
const isPast = getSessionEndDate(aula) < now;
|
|
2402
|
+
return (
|
|
2403
|
+
<Card
|
|
2404
|
+
key={aula.id}
|
|
2405
|
+
className="cursor-pointer overflow-hidden border-border/60 transition-all hover:-translate-y-0.5 hover:shadow-md"
|
|
2406
|
+
onClick={() => openPresenca(aula)}
|
|
2407
|
+
>
|
|
2408
|
+
<div className="flex">
|
|
2409
|
+
<div
|
|
2410
|
+
className="w-1 shrink-0"
|
|
2411
|
+
style={{ backgroundColor: sessionColor }}
|
|
2412
|
+
/>
|
|
2413
|
+
<CardContent className="flex-1 p-3.5">
|
|
2414
|
+
<div className="flex items-start justify-between gap-2">
|
|
2415
|
+
<div className="min-w-0">
|
|
2416
|
+
<h4 className="truncate text-sm font-semibold">
|
|
2417
|
+
{aula.titulo}
|
|
2418
|
+
</h4>
|
|
2419
|
+
<p className="text-xs capitalize text-muted-foreground">
|
|
2420
|
+
{format(
|
|
2421
|
+
parseSessionDate(aula.data),
|
|
2422
|
+
'EEE, dd/MM',
|
|
2423
|
+
{ locale: dateLocale }
|
|
2424
|
+
)}
|
|
2425
|
+
{' · '}
|
|
2426
|
+
{aula.horaInicio} – {aula.horaFim}
|
|
2427
|
+
</p>
|
|
2428
|
+
</div>
|
|
2429
|
+
<Badge
|
|
2430
|
+
variant="outline"
|
|
2431
|
+
className={cn(
|
|
2432
|
+
'shrink-0 text-[10px]',
|
|
2433
|
+
isPast
|
|
2434
|
+
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
2435
|
+
: ''
|
|
2436
|
+
)}
|
|
2437
|
+
>
|
|
2438
|
+
{isPast ? (
|
|
2439
|
+
<CheckCircle2 className="mr-1 size-3" />
|
|
2440
|
+
) : aula.tipo === 'online' ? (
|
|
2441
|
+
<Video className="mr-1 size-3" />
|
|
2442
|
+
) : (
|
|
2443
|
+
<MapPin className="mr-1 size-3" />
|
|
2444
|
+
)}
|
|
2445
|
+
{isPast
|
|
2446
|
+
? t('attendance.register')
|
|
2447
|
+
: tClasses(`type.${aula.tipo}`)}
|
|
2448
|
+
</Badge>
|
|
2449
|
+
</div>
|
|
2450
|
+
</CardContent>
|
|
2451
|
+
</div>
|
|
2452
|
+
</Card>
|
|
2453
|
+
);
|
|
2454
|
+
})}
|
|
711
2455
|
</div>
|
|
2456
|
+
)}
|
|
2457
|
+
</TabsContent>
|
|
2458
|
+
</Tabs>
|
|
2459
|
+
</div>
|
|
2460
|
+
|
|
2461
|
+
{/* ── Right: Operational Sidebar ──────────────────────────────── */}
|
|
2462
|
+
<div className="space-y-3 lg:sticky lg:top-4 lg:self-start">
|
|
2463
|
+
{/* Next Session */}
|
|
2464
|
+
<OperationalSidebarCard
|
|
2465
|
+
title={t('sidebar.nextSession') ?? 'Próxima Aula'}
|
|
2466
|
+
>
|
|
2467
|
+
{loading ? (
|
|
2468
|
+
<div className="space-y-2">
|
|
2469
|
+
<Skeleton className="h-4 w-32" />
|
|
2470
|
+
<Skeleton className="h-4 w-24" />
|
|
712
2471
|
</div>
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
{turma.tipo === 'online' ? (
|
|
716
|
-
<Video className="size-5 text-emerald-600" />
|
|
717
|
-
) : (
|
|
718
|
-
<MapPin className="size-5 text-emerald-600" />
|
|
719
|
-
)}
|
|
720
|
-
</div>
|
|
2472
|
+
) : nextSession ? (
|
|
2473
|
+
<div className="space-y-2">
|
|
721
2474
|
<div>
|
|
2475
|
+
<p className="text-sm font-semibold">
|
|
2476
|
+
{nextSession.titulo}
|
|
2477
|
+
</p>
|
|
722
2478
|
<p className="text-xs text-muted-foreground">
|
|
723
|
-
{
|
|
724
|
-
|
|
725
|
-
|
|
2479
|
+
{nextSessionStartsAt &&
|
|
2480
|
+
format(nextSessionStartsAt, "EEEE, dd 'de' MMM", {
|
|
2481
|
+
locale: dateLocale,
|
|
2482
|
+
})}
|
|
726
2483
|
</p>
|
|
727
|
-
|
|
2484
|
+
</div>
|
|
2485
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
2486
|
+
<Clock className="size-3.5 shrink-0" />
|
|
2487
|
+
<span>
|
|
2488
|
+
{nextSession.horaInicio} – {nextSession.horaFim}
|
|
2489
|
+
</span>
|
|
2490
|
+
</div>
|
|
2491
|
+
{nextSession.tipo === 'online' &&
|
|
2492
|
+
(nextSession.meetingUrl || nextSession.local) ? (
|
|
2493
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
2494
|
+
<Video className="size-3.5 shrink-0" />
|
|
728
2495
|
<a
|
|
729
|
-
href={
|
|
2496
|
+
href={nextSession.meetingUrl || nextSession.local}
|
|
730
2497
|
target="_blank"
|
|
731
2498
|
rel="noopener noreferrer"
|
|
732
|
-
className="
|
|
2499
|
+
className="truncate text-blue-600 hover:underline"
|
|
733
2500
|
>
|
|
734
2501
|
{t('info.accessRoom')}
|
|
735
2502
|
</a>
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
</div>
|
|
755
|
-
</CardContent>
|
|
756
|
-
</Card>
|
|
757
|
-
</motion.div>
|
|
758
|
-
|
|
759
|
-
{/* Tabs */}
|
|
760
|
-
<motion.div variants={fadeUp}>
|
|
761
|
-
<Tabs
|
|
762
|
-
value={activeTab}
|
|
763
|
-
onValueChange={setActiveTab}
|
|
764
|
-
className="w-full"
|
|
765
|
-
>
|
|
766
|
-
<TabsList className="mb-4 w-full justify-start overflow-x-auto">
|
|
767
|
-
<TabsTrigger value="alunos" className="gap-2">
|
|
768
|
-
<Users className="size-4" />
|
|
769
|
-
{t('tabs.students')}
|
|
770
|
-
</TabsTrigger>
|
|
771
|
-
<TabsTrigger value="calendario" className="gap-2">
|
|
772
|
-
<CalendarIcon className="size-4" />
|
|
773
|
-
{t('tabs.calendar')}
|
|
774
|
-
</TabsTrigger>
|
|
775
|
-
<TabsTrigger value="presenca" className="gap-2">
|
|
776
|
-
<CheckCircle2 className="size-4" />
|
|
777
|
-
{t('tabs.attendance')}
|
|
778
|
-
</TabsTrigger>
|
|
779
|
-
</TabsList>
|
|
780
|
-
|
|
781
|
-
{/* ── Tab Alunos ────────────────────────────────────────────────── */}
|
|
782
|
-
<TabsContent value="alunos" className="mt-0">
|
|
783
|
-
{/* Actions bar */}
|
|
784
|
-
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
785
|
-
<div className="relative flex-1 max-w-md">
|
|
786
|
-
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
787
|
-
<Input
|
|
788
|
-
placeholder={t('students.searchPlaceholder')}
|
|
789
|
-
value={alunoSearch}
|
|
790
|
-
onChange={(e) => setAlunoSearch(e.target.value)}
|
|
791
|
-
className="pl-9"
|
|
792
|
-
/>
|
|
793
|
-
</div>
|
|
794
|
-
<div className="flex gap-2">
|
|
795
|
-
{selectedAlunos.length > 0 && (
|
|
796
|
-
<Button
|
|
797
|
-
variant="destructive"
|
|
798
|
-
size="sm"
|
|
799
|
-
className="gap-2"
|
|
800
|
-
onClick={handleRemoveSelectedAlunos}
|
|
801
|
-
>
|
|
802
|
-
<UserMinus className="size-4" />
|
|
803
|
-
{t('students.actions.removeSelected', {
|
|
804
|
-
count: selectedAlunos.length,
|
|
805
|
-
})}
|
|
806
|
-
</Button>
|
|
2503
|
+
</div>
|
|
2504
|
+
) : nextSession.local ? (
|
|
2505
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
2506
|
+
<MapPin className="size-3.5 shrink-0" />
|
|
2507
|
+
<span className="truncate">{nextSession.local}</span>
|
|
2508
|
+
</div>
|
|
2509
|
+
) : null}
|
|
2510
|
+
{nextSession.instructorName && (
|
|
2511
|
+
<div className="flex items-center gap-2 pt-1">
|
|
2512
|
+
<Avatar className="size-6">
|
|
2513
|
+
<AvatarFallback className="text-[9px]">
|
|
2514
|
+
{getPersonInitials(nextSession.instructorName)}
|
|
2515
|
+
</AvatarFallback>
|
|
2516
|
+
</Avatar>
|
|
2517
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
2518
|
+
{nextSession.instructorName}
|
|
2519
|
+
</span>
|
|
2520
|
+
</div>
|
|
807
2521
|
)}
|
|
808
2522
|
<Button
|
|
809
2523
|
size="sm"
|
|
810
|
-
className="gap-2"
|
|
811
|
-
onClick={() =>
|
|
2524
|
+
className="mt-1 w-full gap-2 text-xs"
|
|
2525
|
+
onClick={() => openAulaSheet(nextSession)}
|
|
812
2526
|
>
|
|
813
|
-
<
|
|
814
|
-
{t('
|
|
2527
|
+
<CalendarIcon className="size-3.5" />
|
|
2528
|
+
{t('sidebar.openSession') ?? 'Abrir sessão'}
|
|
815
2529
|
</Button>
|
|
816
2530
|
</div>
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
checked={selectedAlunos.length === filteredAlunos.length}
|
|
824
|
-
onCheckedChange={(checked) =>
|
|
825
|
-
setSelectedAlunos(
|
|
826
|
-
checked ? filteredAlunos.map((a) => a.id) : []
|
|
827
|
-
)
|
|
828
|
-
}
|
|
829
|
-
/>
|
|
830
|
-
<span>
|
|
831
|
-
{t('students.selectedCount', {
|
|
832
|
-
count: selectedAlunos.length,
|
|
833
|
-
})}
|
|
834
|
-
</span>
|
|
2531
|
+
) : (
|
|
2532
|
+
<div className="space-y-2">
|
|
2533
|
+
<p className="text-sm text-muted-foreground">
|
|
2534
|
+
{t('sidebar.noNextSession') ??
|
|
2535
|
+
'Nenhuma aula futura agendada.'}
|
|
2536
|
+
</p>
|
|
835
2537
|
<Button
|
|
836
|
-
variant="ghost"
|
|
837
2538
|
size="sm"
|
|
838
|
-
|
|
2539
|
+
variant="outline"
|
|
2540
|
+
className="w-full gap-2 text-xs"
|
|
2541
|
+
onClick={() => openAulaSheet()}
|
|
839
2542
|
>
|
|
840
|
-
|
|
2543
|
+
<Plus className="size-3.5" />
|
|
2544
|
+
{t('actions.newLesson')}
|
|
841
2545
|
</Button>
|
|
842
2546
|
</div>
|
|
843
2547
|
)}
|
|
2548
|
+
</OperationalSidebarCard>
|
|
844
2549
|
|
|
845
|
-
|
|
2550
|
+
{/* Instructor */}
|
|
2551
|
+
<OperationalSidebarCard
|
|
2552
|
+
title={t('sidebar.instructor') ?? 'Instrutor'}
|
|
2553
|
+
>
|
|
846
2554
|
{loading ? (
|
|
847
|
-
<div className="
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
2555
|
+
<div className="flex items-center gap-2">
|
|
2556
|
+
<Skeleton className="size-8 rounded-full" />
|
|
2557
|
+
<Skeleton className="h-4 w-24" />
|
|
2558
|
+
</div>
|
|
2559
|
+
) : primaryInstructor !== 'Nao definido' ? (
|
|
2560
|
+
<div className="flex items-center gap-2.5">
|
|
2561
|
+
<Avatar className="size-9">
|
|
2562
|
+
<AvatarFallback className="bg-primary/10 text-xs font-medium text-primary">
|
|
2563
|
+
{getPersonInitials(primaryInstructor)}
|
|
2564
|
+
</AvatarFallback>
|
|
2565
|
+
</Avatar>
|
|
2566
|
+
<div className="min-w-0 flex-1">
|
|
2567
|
+
<p className="truncate text-sm font-semibold">
|
|
2568
|
+
{primaryInstructor}
|
|
2569
|
+
</p>
|
|
2570
|
+
<p className="text-[11px] text-muted-foreground">
|
|
2571
|
+
{t('sidebar.instructorLabel') ?? 'Instrutor principal'}
|
|
2572
|
+
</p>
|
|
2573
|
+
</div>
|
|
857
2574
|
</div>
|
|
858
|
-
) : filteredAlunos.length === 0 ? (
|
|
859
|
-
<EmptyState
|
|
860
|
-
icon={<Users className="h-12 w-12" />}
|
|
861
|
-
title={
|
|
862
|
-
alunoSearch
|
|
863
|
-
? t('students.empty.notFound')
|
|
864
|
-
: t('students.empty.notEnrolled')
|
|
865
|
-
}
|
|
866
|
-
description={
|
|
867
|
-
alunoSearch
|
|
868
|
-
? t('students.empty.notFoundDescription')
|
|
869
|
-
: t('students.empty.notEnrolledDescription')
|
|
870
|
-
}
|
|
871
|
-
actionLabel={
|
|
872
|
-
alunoSearch
|
|
873
|
-
? t('students.empty.clearSearch')
|
|
874
|
-
: t('students.actions.addStudent')
|
|
875
|
-
}
|
|
876
|
-
onAction={() => {
|
|
877
|
-
if (alunoSearch) {
|
|
878
|
-
setAlunoSearch('');
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
setAddAlunoDialogOpen(true);
|
|
883
|
-
}}
|
|
884
|
-
/>
|
|
885
2575
|
) : (
|
|
886
|
-
<div className="
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
onClick={(e) => toggleSelectAluno(aluno.id, e)}
|
|
894
|
-
>
|
|
895
|
-
<CardContent className="p-4">
|
|
896
|
-
<div className="flex items-start gap-3">
|
|
897
|
-
<div className="relative">
|
|
898
|
-
<Avatar className="size-12">
|
|
899
|
-
<AvatarImage src={aluno.avatar} />
|
|
900
|
-
<AvatarFallback className="bg-linear-to-br from-blue-100 to-blue-200 text-blue-700 font-medium">
|
|
901
|
-
{aluno.nome
|
|
902
|
-
.split(' ')
|
|
903
|
-
.map((n) => n[0])
|
|
904
|
-
.join('')
|
|
905
|
-
.slice(0, 2)}
|
|
906
|
-
</AvatarFallback>
|
|
907
|
-
</Avatar>
|
|
908
|
-
{isSelected && (
|
|
909
|
-
<div className="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
910
|
-
<Check className="size-3" />
|
|
911
|
-
</div>
|
|
912
|
-
)}
|
|
913
|
-
</div>
|
|
914
|
-
<div className="flex-1 min-w-0">
|
|
915
|
-
<div className="flex items-start justify-between gap-2">
|
|
916
|
-
<div>
|
|
917
|
-
<h4 className="font-semibold truncate">
|
|
918
|
-
{aluno.nome}
|
|
919
|
-
</h4>
|
|
920
|
-
<p className="text-xs text-muted-foreground truncate">
|
|
921
|
-
{aluno.email}
|
|
922
|
-
</p>
|
|
923
|
-
</div>
|
|
924
|
-
<DropdownMenu>
|
|
925
|
-
<DropdownMenuTrigger asChild>
|
|
926
|
-
<Button
|
|
927
|
-
variant="ghost"
|
|
928
|
-
size="icon"
|
|
929
|
-
className="size-8"
|
|
930
|
-
onClick={(e) => e.stopPropagation()}
|
|
931
|
-
>
|
|
932
|
-
<MoreHorizontal className="size-4" />
|
|
933
|
-
</Button>
|
|
934
|
-
</DropdownMenuTrigger>
|
|
935
|
-
<DropdownMenuContent align="end">
|
|
936
|
-
<DropdownMenuItem>
|
|
937
|
-
<Eye className="mr-2 size-4" />
|
|
938
|
-
{t('students.menu.viewProfile')}
|
|
939
|
-
</DropdownMenuItem>
|
|
940
|
-
<DropdownMenuItem>
|
|
941
|
-
<Mail className="mr-2 size-4" />
|
|
942
|
-
{t('students.menu.sendEmail')}
|
|
943
|
-
</DropdownMenuItem>
|
|
944
|
-
<DropdownMenuSeparator />
|
|
945
|
-
<DropdownMenuItem
|
|
946
|
-
className="text-destructive"
|
|
947
|
-
onClick={(e) => {
|
|
948
|
-
e.stopPropagation();
|
|
949
|
-
setAlunoToRemove(aluno);
|
|
950
|
-
setRemoveAlunoDialogOpen(true);
|
|
951
|
-
}}
|
|
952
|
-
>
|
|
953
|
-
<UserMinus className="mr-2 size-4" />
|
|
954
|
-
{t('students.menu.removeFromClass')}
|
|
955
|
-
</DropdownMenuItem>
|
|
956
|
-
</DropdownMenuContent>
|
|
957
|
-
</DropdownMenu>
|
|
958
|
-
</div>
|
|
959
|
-
</div>
|
|
960
|
-
</div>
|
|
961
|
-
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
962
|
-
<div className="rounded-lg bg-muted/50 p-2 text-center">
|
|
963
|
-
<p className="text-lg font-bold text-blue-600">
|
|
964
|
-
{aluno.progresso}%
|
|
965
|
-
</p>
|
|
966
|
-
<p className="text-[10px] text-muted-foreground">
|
|
967
|
-
{t('students.progress')}
|
|
968
|
-
</p>
|
|
969
|
-
</div>
|
|
970
|
-
<div className="rounded-lg bg-muted/50 p-2 text-center">
|
|
971
|
-
<p
|
|
972
|
-
className={`text-lg font-bold ${aluno.presenca >= 75 ? 'text-emerald-600' : aluno.presenca >= 50 ? 'text-amber-600' : 'text-red-600'}`}
|
|
973
|
-
>
|
|
974
|
-
{aluno.presenca}%
|
|
975
|
-
</p>
|
|
976
|
-
<p className="text-[10px] text-muted-foreground">
|
|
977
|
-
{t('students.attendance')}
|
|
978
|
-
</p>
|
|
979
|
-
</div>
|
|
980
|
-
</div>
|
|
981
|
-
</CardContent>
|
|
982
|
-
</Card>
|
|
983
|
-
);
|
|
984
|
-
})}
|
|
2576
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
2577
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-full border border-dashed border-border/60 bg-muted/40">
|
|
2578
|
+
<Users className="size-4" />
|
|
2579
|
+
</div>
|
|
2580
|
+
<p className="text-xs">
|
|
2581
|
+
{t('sidebar.noInstructor') ?? 'Não definido'}
|
|
2582
|
+
</p>
|
|
985
2583
|
</div>
|
|
986
2584
|
)}
|
|
987
|
-
</
|
|
2585
|
+
</OperationalSidebarCard>
|
|
988
2586
|
|
|
989
|
-
{/*
|
|
990
|
-
<
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
<Calendar
|
|
1008
|
-
localizer={localizer}
|
|
1009
|
-
events={calendarEvents}
|
|
1010
|
-
startAccessor="start"
|
|
1011
|
-
endAccessor="end"
|
|
1012
|
-
view={calendarView}
|
|
1013
|
-
onView={(v) => setCalendarView(v)}
|
|
1014
|
-
date={calendarDate}
|
|
1015
|
-
onNavigate={(d) => setCalendarDate(d)}
|
|
1016
|
-
views={['month', 'week']}
|
|
1017
|
-
messages={calendarMessages}
|
|
1018
|
-
eventPropGetter={eventStyleGetter}
|
|
1019
|
-
onSelectEvent={handleSelectEvent}
|
|
1020
|
-
culture={calendarCulture}
|
|
1021
|
-
popup
|
|
1022
|
-
selectable
|
|
1023
|
-
style={{ height: '100%' }}
|
|
1024
|
-
/>
|
|
2587
|
+
{/* Class Info */}
|
|
2588
|
+
<OperationalSidebarCard
|
|
2589
|
+
title={t('sidebar.classInfo') ?? 'Informações da Turma'}
|
|
2590
|
+
>
|
|
2591
|
+
<div className="space-y-2.5">
|
|
2592
|
+
{startDate && endDate && (
|
|
2593
|
+
<div className="flex items-start gap-2 text-xs">
|
|
2594
|
+
<CalendarIcon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
2595
|
+
<div className="min-w-0">
|
|
2596
|
+
<p className="font-medium text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
2597
|
+
{t('info.period')}
|
|
2598
|
+
</p>
|
|
2599
|
+
<p className="font-medium">
|
|
2600
|
+
{format(new Date(startDate), 'dd/MM/yy')}
|
|
2601
|
+
{' – '}
|
|
2602
|
+
{format(new Date(endDate), 'dd/MM/yy')}
|
|
2603
|
+
</p>
|
|
2604
|
+
</div>
|
|
1025
2605
|
</div>
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2606
|
+
)}
|
|
2607
|
+
{schedule !== '—' && (
|
|
2608
|
+
<div className="flex items-start gap-2 text-xs">
|
|
2609
|
+
<Clock className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
2610
|
+
<div className="min-w-0">
|
|
2611
|
+
<p className="font-medium text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
2612
|
+
{t('info.schedule')}
|
|
2613
|
+
</p>
|
|
2614
|
+
<p className="font-medium">{schedule}</p>
|
|
2615
|
+
</div>
|
|
2616
|
+
</div>
|
|
2617
|
+
)}
|
|
2618
|
+
<div className="flex items-start gap-2 text-xs">
|
|
2619
|
+
{isOnline ? (
|
|
2620
|
+
<Video className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
2621
|
+
) : (
|
|
2622
|
+
<MapPin className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
2623
|
+
)}
|
|
2624
|
+
<div className="min-w-0 flex-1">
|
|
2625
|
+
<p className="font-medium text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
2626
|
+
{isOnline
|
|
2627
|
+
? t('info.onlineLabel')
|
|
2628
|
+
: t('info.locationLabel')}
|
|
2629
|
+
</p>
|
|
2630
|
+
{isOnline && roomUrl ? (
|
|
2631
|
+
<div className="flex items-center gap-1">
|
|
2632
|
+
<a
|
|
2633
|
+
href={roomUrl}
|
|
2634
|
+
target="_blank"
|
|
2635
|
+
rel="noopener noreferrer"
|
|
2636
|
+
className="truncate font-medium text-blue-600 hover:underline"
|
|
1053
2637
|
>
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
variant="ghost"
|
|
1088
|
-
size="sm"
|
|
1089
|
-
className="h-7 gap-1 text-xs"
|
|
1090
|
-
>
|
|
1091
|
-
<CheckCircle2 className="size-3" />
|
|
1092
|
-
{t('attendance.register')}
|
|
1093
|
-
</Button>
|
|
1094
|
-
</div>
|
|
1095
|
-
</CardContent>
|
|
1096
|
-
</Card>
|
|
1097
|
-
))}
|
|
2638
|
+
{t('info.accessRoom')}
|
|
2639
|
+
</a>
|
|
2640
|
+
<Button
|
|
2641
|
+
variant="ghost"
|
|
2642
|
+
size="icon"
|
|
2643
|
+
className="size-5 shrink-0"
|
|
2644
|
+
onClick={() => {
|
|
2645
|
+
void navigator.clipboard.writeText(roomUrl);
|
|
2646
|
+
toast.success(
|
|
2647
|
+
t('sidebar.linkCopied') ?? 'Link copiado!'
|
|
2648
|
+
);
|
|
2649
|
+
}}
|
|
2650
|
+
aria-label="Copiar link"
|
|
2651
|
+
>
|
|
2652
|
+
<Save className="size-3" />
|
|
2653
|
+
</Button>
|
|
2654
|
+
</div>
|
|
2655
|
+
) : (
|
|
2656
|
+
<p className="font-medium">{location}</p>
|
|
2657
|
+
)}
|
|
2658
|
+
</div>
|
|
2659
|
+
</div>
|
|
2660
|
+
<div className="flex items-start gap-2 text-xs">
|
|
2661
|
+
<Monitor className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
2662
|
+
<div className="min-w-0">
|
|
2663
|
+
<p className="font-medium text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
2664
|
+
{t('info.modality')}
|
|
2665
|
+
</p>
|
|
2666
|
+
<p className="font-medium capitalize">
|
|
2667
|
+
{tClasses(`type.${deliveryTypeKey}`)}
|
|
2668
|
+
</p>
|
|
2669
|
+
</div>
|
|
2670
|
+
</div>
|
|
1098
2671
|
</div>
|
|
1099
|
-
</
|
|
1100
|
-
|
|
2672
|
+
</OperationalSidebarCard>
|
|
2673
|
+
|
|
2674
|
+
{/* Occupancy */}
|
|
2675
|
+
{capacity > 0 && (
|
|
2676
|
+
<OperationalSidebarCard
|
|
2677
|
+
title={t('sidebar.occupancy') ?? 'Ocupação'}
|
|
2678
|
+
>
|
|
2679
|
+
<div>
|
|
2680
|
+
<div className="mb-2 flex items-baseline justify-between">
|
|
2681
|
+
<div className="flex items-baseline gap-1">
|
|
2682
|
+
<span className="text-2xl font-bold tabular-nums">
|
|
2683
|
+
{occupiedSeats}
|
|
2684
|
+
</span>
|
|
2685
|
+
<span className="text-sm text-muted-foreground">
|
|
2686
|
+
/ {capacity}
|
|
2687
|
+
</span>
|
|
2688
|
+
</div>
|
|
2689
|
+
<span
|
|
2690
|
+
className={cn(
|
|
2691
|
+
'text-sm font-semibold tabular-nums',
|
|
2692
|
+
occupancyRate >= 90
|
|
2693
|
+
? 'text-red-600'
|
|
2694
|
+
: occupancyRate >= 70
|
|
2695
|
+
? 'text-amber-600'
|
|
2696
|
+
: 'text-emerald-600'
|
|
2697
|
+
)}
|
|
2698
|
+
>
|
|
2699
|
+
{occupancyRate}%
|
|
2700
|
+
</span>
|
|
2701
|
+
</div>
|
|
2702
|
+
<div className="mb-2 h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
2703
|
+
<div
|
|
2704
|
+
className={cn(
|
|
2705
|
+
'h-full rounded-full transition-all',
|
|
2706
|
+
occupancyRate >= 90
|
|
2707
|
+
? 'bg-red-500'
|
|
2708
|
+
: occupancyRate >= 70
|
|
2709
|
+
? 'bg-amber-500'
|
|
2710
|
+
: 'bg-emerald-500'
|
|
2711
|
+
)}
|
|
2712
|
+
style={{ width: `${occupancyRate}%` }}
|
|
2713
|
+
/>
|
|
2714
|
+
</div>
|
|
2715
|
+
<p className="text-xs text-muted-foreground">
|
|
2716
|
+
{availableSeats > 0
|
|
2717
|
+
? `${availableSeats} ${t('sidebar.seatsAvailable') ?? 'vagas disponíveis'}`
|
|
2718
|
+
: (t('sidebar.classFull') ?? 'Turma lotada')}
|
|
2719
|
+
</p>
|
|
2720
|
+
<Button
|
|
2721
|
+
size="sm"
|
|
2722
|
+
variant="outline"
|
|
2723
|
+
className="mt-2 w-full gap-2 text-xs"
|
|
2724
|
+
onClick={() => setActiveTab('alunos')}
|
|
2725
|
+
>
|
|
2726
|
+
<Users className="size-3.5" />
|
|
2727
|
+
{t('sidebar.viewStudents') ?? 'Ver alunos'}
|
|
2728
|
+
</Button>
|
|
2729
|
+
</div>
|
|
2730
|
+
</OperationalSidebarCard>
|
|
2731
|
+
)}
|
|
2732
|
+
</div>
|
|
1101
2733
|
</motion.div>
|
|
1102
2734
|
</motion.div>
|
|
1103
2735
|
</div>
|
|
1104
2736
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
2737
|
+
<CreateLmsStudentPersonSheet
|
|
2738
|
+
open={createStudentDialogOpen}
|
|
2739
|
+
onOpenChange={setCreateStudentDialogOpen}
|
|
2740
|
+
classGroupId={id}
|
|
2741
|
+
onEnrolled={handleStudentEnrolled}
|
|
2742
|
+
title={t('dialogs.createStudent.title')}
|
|
2743
|
+
description={t('dialogs.createStudent.description')}
|
|
2744
|
+
errorMessage={t('toasts.error')}
|
|
2745
|
+
alreadyEnrolledMessage="Esta pessoa ja esta matriculada na turma."
|
|
2746
|
+
/>
|
|
2747
|
+
|
|
2748
|
+
<Dialog
|
|
2749
|
+
open={studentProfileDialogOpen}
|
|
2750
|
+
onOpenChange={setStudentProfileDialogOpen}
|
|
2751
|
+
>
|
|
2752
|
+
<DialogContent className="max-w-lg">
|
|
1108
2753
|
<DialogHeader>
|
|
1109
|
-
<DialogTitle>{t('dialogs.
|
|
2754
|
+
<DialogTitle>{t('dialogs.studentProfile.title')}</DialogTitle>
|
|
1110
2755
|
<DialogDescription>
|
|
1111
|
-
{t('dialogs.
|
|
2756
|
+
{t('dialogs.studentProfile.description')}
|
|
1112
2757
|
</DialogDescription>
|
|
1113
2758
|
</DialogHeader>
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
>
|
|
1127
|
-
|
|
1128
|
-
<
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
2759
|
+
|
|
2760
|
+
{loadingStudentProfile ? (
|
|
2761
|
+
<div className="flex items-center justify-center py-8">
|
|
2762
|
+
<Loader2 className="size-5 animate-spin" />
|
|
2763
|
+
</div>
|
|
2764
|
+
) : selectedStudentProfile ? (
|
|
2765
|
+
<div className="space-y-3">
|
|
2766
|
+
<div className="rounded-lg border p-3">
|
|
2767
|
+
<p className="text-xs text-muted-foreground">
|
|
2768
|
+
{t('dialogs.studentProfile.fields.name')}
|
|
2769
|
+
</p>
|
|
2770
|
+
<p className="font-medium">{selectedStudentProfile.nome}</p>
|
|
2771
|
+
</div>
|
|
2772
|
+
<div className="rounded-lg border p-3">
|
|
2773
|
+
<p className="text-xs text-muted-foreground">
|
|
2774
|
+
{t('dialogs.studentProfile.fields.email')}
|
|
2775
|
+
</p>
|
|
2776
|
+
<p className="font-medium">
|
|
2777
|
+
{selectedStudentProfile.email || '—'}
|
|
2778
|
+
</p>
|
|
2779
|
+
</div>
|
|
2780
|
+
<div className="rounded-lg border p-3">
|
|
2781
|
+
<p className="text-xs text-muted-foreground">
|
|
2782
|
+
{t('dialogs.studentProfile.fields.phone')}
|
|
2783
|
+
</p>
|
|
2784
|
+
<p className="font-medium">
|
|
2785
|
+
{selectedStudentProfile.telefone || '—'}
|
|
2786
|
+
</p>
|
|
2787
|
+
</div>
|
|
2788
|
+
<div className="grid grid-cols-2 gap-3">
|
|
2789
|
+
<div className="rounded-lg border p-3">
|
|
2790
|
+
<p className="text-xs text-muted-foreground">
|
|
2791
|
+
{t('dialogs.studentProfile.fields.progress')}
|
|
2792
|
+
</p>
|
|
2793
|
+
<p className="font-medium">
|
|
2794
|
+
{selectedStudentProfile.progresso}%
|
|
2795
|
+
</p>
|
|
2796
|
+
</div>
|
|
2797
|
+
<div className="rounded-lg border p-3">
|
|
2798
|
+
<p className="text-xs text-muted-foreground">
|
|
2799
|
+
{t('dialogs.studentProfile.fields.enrolledAt')}
|
|
2800
|
+
</p>
|
|
2801
|
+
<p className="font-medium">
|
|
2802
|
+
{selectedStudentProfile.matriculadoEm
|
|
2803
|
+
? format(
|
|
2804
|
+
parseSessionDate(
|
|
2805
|
+
selectedStudentProfile.matriculadoEm
|
|
2806
|
+
),
|
|
2807
|
+
'dd/MM/yyyy'
|
|
2808
|
+
)
|
|
2809
|
+
: '—'}
|
|
1141
2810
|
</p>
|
|
1142
2811
|
</div>
|
|
1143
2812
|
</div>
|
|
1144
|
-
|
|
1145
|
-
|
|
2813
|
+
</div>
|
|
2814
|
+
) : null}
|
|
2815
|
+
|
|
1146
2816
|
<DialogFooter>
|
|
1147
2817
|
<Button
|
|
1148
2818
|
variant="outline"
|
|
1149
|
-
onClick={() =>
|
|
1150
|
-
setAddAlunoDialogOpen(false);
|
|
1151
|
-
setAlunosToAdd([]);
|
|
1152
|
-
}}
|
|
2819
|
+
onClick={() => setStudentProfileDialogOpen(false)}
|
|
1153
2820
|
>
|
|
1154
2821
|
{t('common.cancel')}
|
|
1155
2822
|
</Button>
|
|
1156
|
-
<Button
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
>
|
|
1160
|
-
{t('dialogs.addStudents.confirm')}{' '}
|
|
1161
|
-
{alunosToAdd.length > 0 && `(${alunosToAdd.length})`}
|
|
2823
|
+
<Button onClick={openEditStudentSheet} className="gap-2">
|
|
2824
|
+
<Pencil className="size-4" />
|
|
2825
|
+
{t('common.edit')}
|
|
1162
2826
|
</Button>
|
|
1163
2827
|
</DialogFooter>
|
|
1164
2828
|
</DialogContent>
|
|
1165
2829
|
</Dialog>
|
|
1166
2830
|
|
|
2831
|
+
<Sheet open={editStudentSheetOpen} onOpenChange={setEditStudentSheetOpen}>
|
|
2832
|
+
<SheetContent
|
|
2833
|
+
side="right"
|
|
2834
|
+
className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
|
|
2835
|
+
>
|
|
2836
|
+
<SheetHeader>
|
|
2837
|
+
<SheetTitle>{t('dialogs.editStudent.title')}</SheetTitle>
|
|
2838
|
+
<SheetDescription>
|
|
2839
|
+
{t('dialogs.editStudent.description')}
|
|
2840
|
+
</SheetDescription>
|
|
2841
|
+
</SheetHeader>
|
|
2842
|
+
|
|
2843
|
+
<Form {...editStudentForm}>
|
|
2844
|
+
<form
|
|
2845
|
+
onSubmit={handleUpdateStudentProfile}
|
|
2846
|
+
className="mt-6 space-y-3 px-4"
|
|
2847
|
+
>
|
|
2848
|
+
<FormField
|
|
2849
|
+
control={editStudentForm.control}
|
|
2850
|
+
name="name"
|
|
2851
|
+
render={({ field }) => (
|
|
2852
|
+
<FormItem>
|
|
2853
|
+
<FormLabel>
|
|
2854
|
+
{t('dialogs.editStudent.fields.name')}
|
|
2855
|
+
</FormLabel>
|
|
2856
|
+
<FormControl>
|
|
2857
|
+
<Input
|
|
2858
|
+
{...field}
|
|
2859
|
+
placeholder={t(
|
|
2860
|
+
'dialogs.editStudent.fields.namePlaceholder'
|
|
2861
|
+
)}
|
|
2862
|
+
/>
|
|
2863
|
+
</FormControl>
|
|
2864
|
+
<FormMessage />
|
|
2865
|
+
</FormItem>
|
|
2866
|
+
)}
|
|
2867
|
+
/>
|
|
2868
|
+
<FormField
|
|
2869
|
+
control={editStudentForm.control}
|
|
2870
|
+
name="email"
|
|
2871
|
+
render={({ field }) => (
|
|
2872
|
+
<FormItem>
|
|
2873
|
+
<FormLabel>
|
|
2874
|
+
{t('dialogs.editStudent.fields.email')}
|
|
2875
|
+
</FormLabel>
|
|
2876
|
+
<FormControl>
|
|
2877
|
+
<Input
|
|
2878
|
+
type="email"
|
|
2879
|
+
{...field}
|
|
2880
|
+
placeholder={t(
|
|
2881
|
+
'dialogs.editStudent.fields.emailPlaceholder'
|
|
2882
|
+
)}
|
|
2883
|
+
/>
|
|
2884
|
+
</FormControl>
|
|
2885
|
+
<FormMessage />
|
|
2886
|
+
</FormItem>
|
|
2887
|
+
)}
|
|
2888
|
+
/>
|
|
2889
|
+
<FormField
|
|
2890
|
+
control={editStudentForm.control}
|
|
2891
|
+
name="phone"
|
|
2892
|
+
render={({ field }) => (
|
|
2893
|
+
<FormItem>
|
|
2894
|
+
<FormLabel>
|
|
2895
|
+
{t('dialogs.editStudent.fields.phone')}
|
|
2896
|
+
</FormLabel>
|
|
2897
|
+
<FormControl>
|
|
2898
|
+
<Input
|
|
2899
|
+
{...field}
|
|
2900
|
+
placeholder={t(
|
|
2901
|
+
'dialogs.editStudent.fields.phonePlaceholder'
|
|
2902
|
+
)}
|
|
2903
|
+
/>
|
|
2904
|
+
</FormControl>
|
|
2905
|
+
<FormMessage />
|
|
2906
|
+
</FormItem>
|
|
2907
|
+
)}
|
|
2908
|
+
/>
|
|
2909
|
+
|
|
2910
|
+
<SheetFooter className="mt-6 px-0">
|
|
2911
|
+
<Button
|
|
2912
|
+
type="button"
|
|
2913
|
+
variant="outline"
|
|
2914
|
+
onClick={() => {
|
|
2915
|
+
setEditStudentSheetOpen(false);
|
|
2916
|
+
setStudentProfileDialogOpen(true);
|
|
2917
|
+
}}
|
|
2918
|
+
>
|
|
2919
|
+
{t('common.cancel')}
|
|
2920
|
+
</Button>
|
|
2921
|
+
<Button
|
|
2922
|
+
type="submit"
|
|
2923
|
+
disabled={savingStudentProfile}
|
|
2924
|
+
className="gap-2"
|
|
2925
|
+
>
|
|
2926
|
+
{savingStudentProfile && (
|
|
2927
|
+
<Loader2 className="size-4 animate-spin" />
|
|
2928
|
+
)}
|
|
2929
|
+
{t('common.save')}
|
|
2930
|
+
</Button>
|
|
2931
|
+
</SheetFooter>
|
|
2932
|
+
</form>
|
|
2933
|
+
</Form>
|
|
2934
|
+
</SheetContent>
|
|
2935
|
+
</Sheet>
|
|
2936
|
+
|
|
1167
2937
|
{/* ── Dialog Remover Aluno ─────────────────────────────────────────────── */}
|
|
1168
2938
|
<Dialog
|
|
1169
2939
|
open={removeAlunoDialogOpen}
|
|
@@ -1191,7 +2961,13 @@ export default function TurmaDetalhePage() {
|
|
|
1191
2961
|
>
|
|
1192
2962
|
{t('common.cancel')}
|
|
1193
2963
|
</Button>
|
|
1194
|
-
<Button
|
|
2964
|
+
<Button
|
|
2965
|
+
variant="destructive"
|
|
2966
|
+
onClick={handleRemoveAluno}
|
|
2967
|
+
disabled={removingStudent}
|
|
2968
|
+
className="gap-2"
|
|
2969
|
+
>
|
|
2970
|
+
{removingStudent && <Loader2 className="size-4 animate-spin" />}
|
|
1195
2971
|
{t('common.remove')}
|
|
1196
2972
|
</Button>
|
|
1197
2973
|
</DialogFooter>
|
|
@@ -1200,7 +2976,10 @@ export default function TurmaDetalhePage() {
|
|
|
1200
2976
|
|
|
1201
2977
|
{/* ── Sheet Aula ───────────────────────────────────────────────────────── */}
|
|
1202
2978
|
<Sheet open={aulaSheetOpen} onOpenChange={setAulaSheetOpen}>
|
|
1203
|
-
<SheetContent
|
|
2979
|
+
<SheetContent
|
|
2980
|
+
side="right"
|
|
2981
|
+
className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
|
|
2982
|
+
>
|
|
1204
2983
|
<SheetHeader>
|
|
1205
2984
|
<SheetTitle>
|
|
1206
2985
|
{editingAula
|
|
@@ -1213,196 +2992,554 @@ export default function TurmaDetalhePage() {
|
|
|
1213
2992
|
: t('sheet.lessonForm.descriptionCreate')}
|
|
1214
2993
|
</SheetDescription>
|
|
1215
2994
|
</SheetHeader>
|
|
1216
|
-
<
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
</
|
|
1229
|
-
|
|
1230
|
-
<
|
|
1231
|
-
<
|
|
1232
|
-
<
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
{
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
<Controller
|
|
1243
|
-
name="tipo"
|
|
1244
|
-
control={aulaForm.control}
|
|
1245
|
-
render={({ field }) => (
|
|
1246
|
-
<Select value={field.value} onValueChange={field.onChange}>
|
|
1247
|
-
<SelectTrigger>
|
|
1248
|
-
<SelectValue
|
|
1249
|
-
placeholder={t('sheet.lessonForm.fields.select')}
|
|
1250
|
-
/>
|
|
1251
|
-
</SelectTrigger>
|
|
1252
|
-
<SelectContent>
|
|
1253
|
-
<SelectItem value="online">
|
|
1254
|
-
{tClasses('type.online')}
|
|
1255
|
-
</SelectItem>
|
|
1256
|
-
<SelectItem value="presencial">
|
|
1257
|
-
{tClasses('type.presencial')}
|
|
1258
|
-
</SelectItem>
|
|
1259
|
-
</SelectContent>
|
|
1260
|
-
</Select>
|
|
2995
|
+
<Tabs
|
|
2996
|
+
value={aulaSheetTab}
|
|
2997
|
+
onValueChange={(value) =>
|
|
2998
|
+
setAulaSheetTab(value as 'aulas' | 'chamada')
|
|
2999
|
+
}
|
|
3000
|
+
className="mt-6 px-4"
|
|
3001
|
+
>
|
|
3002
|
+
<TabsList className="grid w-full grid-cols-2">
|
|
3003
|
+
<TabsTrigger value="aulas">{t('sheet.tabs.lessons')}</TabsTrigger>
|
|
3004
|
+
<TabsTrigger value="chamada">
|
|
3005
|
+
{t('sheet.tabs.attendance')}
|
|
3006
|
+
</TabsTrigger>
|
|
3007
|
+
</TabsList>
|
|
3008
|
+
|
|
3009
|
+
<TabsContent value="aulas" className="mt-4">
|
|
3010
|
+
<form onSubmit={handleSaveAula} className="space-y-5">
|
|
3011
|
+
<Field>
|
|
3012
|
+
<FieldLabel>{t('sheet.lessonForm.fields.title')}</FieldLabel>
|
|
3013
|
+
<Input
|
|
3014
|
+
{...aulaForm.register('titulo')}
|
|
3015
|
+
placeholder={t('sheet.lessonForm.fields.titlePlaceholder')}
|
|
3016
|
+
/>
|
|
3017
|
+
{aulaForm.formState.errors.titulo && (
|
|
3018
|
+
<FieldError>
|
|
3019
|
+
{aulaForm.formState.errors.titulo.message}
|
|
3020
|
+
</FieldError>
|
|
1261
3021
|
)}
|
|
1262
|
-
|
|
1263
|
-
</Field>
|
|
1264
|
-
</div>
|
|
3022
|
+
</Field>
|
|
1265
3023
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
3024
|
+
<div className="grid grid-cols-2 gap-4">
|
|
3025
|
+
<Field>
|
|
3026
|
+
<FieldLabel>{t('sheet.lessonForm.fields.date')}</FieldLabel>
|
|
3027
|
+
<Input type="date" {...aulaForm.register('data')} />
|
|
3028
|
+
{aulaForm.formState.errors.data && (
|
|
3029
|
+
<FieldError>
|
|
3030
|
+
{aulaForm.formState.errors.data.message}
|
|
3031
|
+
</FieldError>
|
|
3032
|
+
)}
|
|
3033
|
+
</Field>
|
|
3034
|
+
<Field>
|
|
3035
|
+
<FieldLabel>{t('sheet.lessonForm.fields.type')}</FieldLabel>
|
|
3036
|
+
<Controller
|
|
3037
|
+
name="tipo"
|
|
3038
|
+
control={aulaForm.control}
|
|
3039
|
+
render={({ field }) => (
|
|
3040
|
+
<Select
|
|
3041
|
+
value={field.value}
|
|
3042
|
+
onValueChange={field.onChange}
|
|
3043
|
+
>
|
|
3044
|
+
<SelectTrigger>
|
|
3045
|
+
<SelectValue
|
|
3046
|
+
placeholder={t('sheet.lessonForm.fields.select')}
|
|
3047
|
+
/>
|
|
3048
|
+
</SelectTrigger>
|
|
3049
|
+
<SelectContent>
|
|
3050
|
+
<SelectItem value="online">
|
|
3051
|
+
{tClasses('type.online')}
|
|
3052
|
+
</SelectItem>
|
|
3053
|
+
<SelectItem value="presencial">
|
|
3054
|
+
{tClasses('type.presencial')}
|
|
3055
|
+
</SelectItem>
|
|
3056
|
+
</SelectContent>
|
|
3057
|
+
</Select>
|
|
3058
|
+
)}
|
|
3059
|
+
/>
|
|
3060
|
+
</Field>
|
|
3061
|
+
</div>
|
|
1278
3062
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
<Button type="submit">
|
|
1294
|
-
{editingAula
|
|
1295
|
-
? t('sheet.lessonForm.actions.save')
|
|
1296
|
-
: t('sheet.lessonForm.actions.create')}
|
|
1297
|
-
</Button>
|
|
1298
|
-
</SheetFooter>
|
|
1299
|
-
</form>
|
|
1300
|
-
</SheetContent>
|
|
1301
|
-
</Sheet>
|
|
3063
|
+
<div className="grid grid-cols-2 gap-4">
|
|
3064
|
+
<Field>
|
|
3065
|
+
<FieldLabel>
|
|
3066
|
+
{t('sheet.lessonForm.fields.startTime')}
|
|
3067
|
+
</FieldLabel>
|
|
3068
|
+
<Input type="time" {...aulaForm.register('horaInicio')} />
|
|
3069
|
+
</Field>
|
|
3070
|
+
<Field>
|
|
3071
|
+
<FieldLabel>
|
|
3072
|
+
{t('sheet.lessonForm.fields.endTime')}
|
|
3073
|
+
</FieldLabel>
|
|
3074
|
+
<Input type="time" {...aulaForm.register('horaFim')} />
|
|
3075
|
+
</Field>
|
|
3076
|
+
</div>
|
|
1302
3077
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
</>
|
|
1314
|
-
)}
|
|
1315
|
-
</SheetDescription>
|
|
1316
|
-
</SheetHeader>
|
|
3078
|
+
<Field>
|
|
3079
|
+
<FieldLabel>{t('sheet.lessonForm.fields.color')}</FieldLabel>
|
|
3080
|
+
<Controller
|
|
3081
|
+
name="cor"
|
|
3082
|
+
control={aulaForm.control}
|
|
3083
|
+
render={({ field }) => (
|
|
3084
|
+
<>
|
|
3085
|
+
<div className="flex flex-wrap gap-2">
|
|
3086
|
+
{SESSION_COLOR_PALETTE.map((colorOption) => {
|
|
3087
|
+
const selected = field.value === colorOption;
|
|
1317
3088
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
prev.map((p) => ({ ...p, presente: false }))
|
|
1344
|
-
)
|
|
1345
|
-
}
|
|
1346
|
-
>
|
|
1347
|
-
{t('sheet.attendance.allAbsent')}
|
|
1348
|
-
</Button>
|
|
1349
|
-
</div>
|
|
1350
|
-
</div>
|
|
3089
|
+
return (
|
|
3090
|
+
<button
|
|
3091
|
+
key={colorOption}
|
|
3092
|
+
type="button"
|
|
3093
|
+
aria-label={colorOption}
|
|
3094
|
+
title={colorOption}
|
|
3095
|
+
className={`h-8 w-8 cursor-pointer rounded-full border-2 transition ${selected ? 'border-foreground scale-110' : 'border-transparent hover:scale-105'}`}
|
|
3096
|
+
style={{ backgroundColor: colorOption }}
|
|
3097
|
+
onClick={() => field.onChange(colorOption)}
|
|
3098
|
+
/>
|
|
3099
|
+
);
|
|
3100
|
+
})}
|
|
3101
|
+
</div>
|
|
3102
|
+
<p className="text-xs text-muted-foreground">
|
|
3103
|
+
{t('sheet.lessonForm.fields.colorHint')}
|
|
3104
|
+
</p>
|
|
3105
|
+
</>
|
|
3106
|
+
)}
|
|
3107
|
+
/>
|
|
3108
|
+
{aulaForm.formState.errors.cor && (
|
|
3109
|
+
<FieldError>
|
|
3110
|
+
{aulaForm.formState.errors.cor.message}
|
|
3111
|
+
</FieldError>
|
|
3112
|
+
)}
|
|
3113
|
+
</Field>
|
|
1351
3114
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
3115
|
+
{showRecurrenceFields ? (
|
|
3116
|
+
<div className="space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
|
|
3117
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
3118
|
+
<Field>
|
|
3119
|
+
<FieldLabel>
|
|
3120
|
+
{t('sheet.lessonForm.recurrence.label')}
|
|
3121
|
+
</FieldLabel>
|
|
3122
|
+
<Controller
|
|
3123
|
+
name="recurrenceFrequency"
|
|
3124
|
+
control={aulaForm.control}
|
|
3125
|
+
render={({ field }) => (
|
|
3126
|
+
<Select
|
|
3127
|
+
value={field.value}
|
|
3128
|
+
onValueChange={(value) =>
|
|
3129
|
+
field.onChange(
|
|
3130
|
+
value as SessionRecurrenceFrequency
|
|
3131
|
+
)
|
|
3132
|
+
}
|
|
3133
|
+
>
|
|
3134
|
+
<SelectTrigger>
|
|
3135
|
+
<SelectValue />
|
|
3136
|
+
</SelectTrigger>
|
|
3137
|
+
<SelectContent>
|
|
3138
|
+
{!editingAula?.isRecurring ? (
|
|
3139
|
+
<SelectItem value="none">
|
|
3140
|
+
{t(
|
|
3141
|
+
'sheet.lessonForm.recurrence.options.none'
|
|
3142
|
+
)}
|
|
3143
|
+
</SelectItem>
|
|
3144
|
+
) : null}
|
|
3145
|
+
<SelectItem value="daily">
|
|
3146
|
+
{t(
|
|
3147
|
+
'sheet.lessonForm.recurrence.options.daily'
|
|
3148
|
+
)}
|
|
3149
|
+
</SelectItem>
|
|
3150
|
+
<SelectItem value="weekly">
|
|
3151
|
+
{t(
|
|
3152
|
+
'sheet.lessonForm.recurrence.options.weekly'
|
|
3153
|
+
)}
|
|
3154
|
+
</SelectItem>
|
|
3155
|
+
<SelectItem value="monthly">
|
|
3156
|
+
{t(
|
|
3157
|
+
'sheet.lessonForm.recurrence.options.monthly'
|
|
3158
|
+
)}
|
|
3159
|
+
</SelectItem>
|
|
3160
|
+
<SelectItem value="yearly">
|
|
3161
|
+
{t(
|
|
3162
|
+
'sheet.lessonForm.recurrence.options.yearly'
|
|
3163
|
+
)}
|
|
3164
|
+
</SelectItem>
|
|
3165
|
+
</SelectContent>
|
|
3166
|
+
</Select>
|
|
3167
|
+
)}
|
|
3168
|
+
/>
|
|
3169
|
+
</Field>
|
|
3170
|
+
|
|
3171
|
+
{recurrenceFrequency !== 'none' ? (
|
|
3172
|
+
<Field>
|
|
3173
|
+
<FieldLabel>
|
|
3174
|
+
{t('sheet.lessonForm.recurrence.until')}
|
|
3175
|
+
</FieldLabel>
|
|
3176
|
+
<Input
|
|
3177
|
+
type="date"
|
|
3178
|
+
{...aulaForm.register('recurrenceUntil')}
|
|
3179
|
+
/>
|
|
3180
|
+
{aulaForm.formState.errors.recurrenceUntil && (
|
|
3181
|
+
<FieldError>
|
|
3182
|
+
{
|
|
3183
|
+
aulaForm.formState.errors.recurrenceUntil
|
|
3184
|
+
.message
|
|
3185
|
+
}
|
|
3186
|
+
</FieldError>
|
|
3187
|
+
)}
|
|
3188
|
+
</Field>
|
|
3189
|
+
) : null}
|
|
3190
|
+
</div>
|
|
3191
|
+
|
|
3192
|
+
{recurrenceFrequency === 'weekly' ? (
|
|
3193
|
+
<Field>
|
|
3194
|
+
<FieldLabel>
|
|
3195
|
+
{t('sheet.lessonForm.recurrence.daysLabel')}
|
|
3196
|
+
</FieldLabel>
|
|
3197
|
+
<div className="grid grid-cols-4 gap-2 sm:grid-cols-7">
|
|
3198
|
+
{RECURRENCE_DAY_LABELS.map((day) => {
|
|
3199
|
+
const selected = recurrenceDaysOfWeek.includes(
|
|
3200
|
+
day.value
|
|
3201
|
+
);
|
|
3202
|
+
|
|
3203
|
+
return (
|
|
3204
|
+
<Button
|
|
3205
|
+
key={day.value}
|
|
3206
|
+
type="button"
|
|
3207
|
+
variant={selected ? 'default' : 'outline'}
|
|
3208
|
+
size="sm"
|
|
3209
|
+
className="h-9"
|
|
3210
|
+
onClick={() => {
|
|
3211
|
+
const nextValue = selected
|
|
3212
|
+
? recurrenceDaysOfWeek.filter(
|
|
3213
|
+
(value) => value !== day.value
|
|
3214
|
+
)
|
|
3215
|
+
: [...recurrenceDaysOfWeek, day.value];
|
|
3216
|
+
|
|
3217
|
+
aulaForm.setValue(
|
|
3218
|
+
'recurrenceDaysOfWeek',
|
|
3219
|
+
nextValue,
|
|
3220
|
+
{
|
|
3221
|
+
shouldDirty: true,
|
|
3222
|
+
shouldTouch: true,
|
|
3223
|
+
shouldValidate: true,
|
|
3224
|
+
}
|
|
3225
|
+
);
|
|
3226
|
+
}}
|
|
3227
|
+
>
|
|
3228
|
+
{t(day.labelKey)}
|
|
3229
|
+
</Button>
|
|
3230
|
+
);
|
|
3231
|
+
})}
|
|
3232
|
+
</div>
|
|
3233
|
+
<p className="text-xs text-muted-foreground">
|
|
3234
|
+
{t('sheet.lessonForm.recurrence.weeklyHint')}
|
|
3235
|
+
</p>
|
|
3236
|
+
</Field>
|
|
3237
|
+
) : null}
|
|
3238
|
+
|
|
3239
|
+
{editingAula?.isRecurring ? (
|
|
3240
|
+
<Field>
|
|
3241
|
+
<FieldLabel>
|
|
3242
|
+
{t('sheet.lessonForm.recurrence.applyScope')}
|
|
3243
|
+
</FieldLabel>
|
|
3244
|
+
<Controller
|
|
3245
|
+
name="applyScope"
|
|
3246
|
+
control={aulaForm.control}
|
|
3247
|
+
render={({ field }) => (
|
|
3248
|
+
<Select
|
|
3249
|
+
value={field.value}
|
|
3250
|
+
onValueChange={field.onChange}
|
|
3251
|
+
>
|
|
3252
|
+
<SelectTrigger>
|
|
3253
|
+
<SelectValue />
|
|
3254
|
+
</SelectTrigger>
|
|
3255
|
+
<SelectContent>
|
|
3256
|
+
<SelectItem value="single">
|
|
3257
|
+
{t(
|
|
3258
|
+
'sheet.lessonForm.recurrence.applyScopeSingle'
|
|
3259
|
+
)}
|
|
3260
|
+
</SelectItem>
|
|
3261
|
+
<SelectItem value="series">
|
|
3262
|
+
{t(
|
|
3263
|
+
'sheet.lessonForm.recurrence.applyScopeSeries'
|
|
3264
|
+
)}
|
|
3265
|
+
</SelectItem>
|
|
3266
|
+
</SelectContent>
|
|
3267
|
+
</Select>
|
|
3268
|
+
)}
|
|
3269
|
+
/>
|
|
3270
|
+
<p className="text-xs text-muted-foreground">
|
|
3271
|
+
{t('sheet.lessonForm.recurrence.applyScopeHint')}
|
|
3272
|
+
</p>
|
|
3273
|
+
</Field>
|
|
3274
|
+
) : null}
|
|
1384
3275
|
</div>
|
|
3276
|
+
) : null}
|
|
3277
|
+
|
|
3278
|
+
<Field>
|
|
3279
|
+
<FieldLabel>
|
|
3280
|
+
{t('sheet.lessonForm.fields.instructor')}
|
|
3281
|
+
</FieldLabel>
|
|
3282
|
+
<Controller
|
|
3283
|
+
name="instrutorId"
|
|
3284
|
+
control={aulaForm.control}
|
|
3285
|
+
render={({ field }) => (
|
|
3286
|
+
<div className="flex items-end gap-2">
|
|
3287
|
+
<div className="flex-1">
|
|
3288
|
+
<Popover
|
|
3289
|
+
open={instructorOpen}
|
|
3290
|
+
onOpenChange={setInstructorOpen}
|
|
3291
|
+
>
|
|
3292
|
+
<PopoverTrigger asChild>
|
|
3293
|
+
<Button
|
|
3294
|
+
type="button"
|
|
3295
|
+
variant="outline"
|
|
3296
|
+
role="combobox"
|
|
3297
|
+
className="w-full justify-between"
|
|
3298
|
+
>
|
|
3299
|
+
<span className="truncate text-left">
|
|
3300
|
+
{instructorOptions.find(
|
|
3301
|
+
(instructor) =>
|
|
3302
|
+
String(instructor.id) === field.value
|
|
3303
|
+
)?.name ||
|
|
3304
|
+
editingAula?.instructorName ||
|
|
3305
|
+
t(
|
|
3306
|
+
'sheet.lessonForm.fields.instructorPlaceholder'
|
|
3307
|
+
)}
|
|
3308
|
+
</span>
|
|
3309
|
+
{loadingInstructors ? (
|
|
3310
|
+
<Loader2 className="ml-2 h-4 w-4 shrink-0 animate-spin opacity-60" />
|
|
3311
|
+
) : (
|
|
3312
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
3313
|
+
)}
|
|
3314
|
+
</Button>
|
|
3315
|
+
</PopoverTrigger>
|
|
3316
|
+
<PopoverContent className="p-0" align="start">
|
|
3317
|
+
<Command shouldFilter={false}>
|
|
3318
|
+
<CommandInput
|
|
3319
|
+
placeholder={t(
|
|
3320
|
+
'sheet.lessonForm.fields.instructorPlaceholder'
|
|
3321
|
+
)}
|
|
3322
|
+
value={instructorSearch}
|
|
3323
|
+
onValueChange={setInstructorSearch}
|
|
3324
|
+
/>
|
|
3325
|
+
<CommandList>
|
|
3326
|
+
<CommandEmpty>
|
|
3327
|
+
<div className="flex flex-col items-center gap-3 px-2 py-4">
|
|
3328
|
+
<p className="text-sm text-muted-foreground">
|
|
3329
|
+
{t(
|
|
3330
|
+
'sheet.lessonForm.fields.instructorNotFound'
|
|
3331
|
+
)}
|
|
3332
|
+
</p>
|
|
3333
|
+
<Button
|
|
3334
|
+
type="button"
|
|
3335
|
+
variant="outline"
|
|
3336
|
+
size="sm"
|
|
3337
|
+
className="w-full"
|
|
3338
|
+
onClick={() => {
|
|
3339
|
+
setInstructorOpen(false);
|
|
3340
|
+
setCreateInstructorDialogOpen(true);
|
|
3341
|
+
}}
|
|
3342
|
+
>
|
|
3343
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
3344
|
+
{t(
|
|
3345
|
+
'sheet.lessonForm.fields.createInstructor'
|
|
3346
|
+
)}
|
|
3347
|
+
</Button>
|
|
3348
|
+
</div>
|
|
3349
|
+
</CommandEmpty>
|
|
3350
|
+
<CommandGroup>
|
|
3351
|
+
{instructorOptions.map((instructor) => (
|
|
3352
|
+
<CommandItem
|
|
3353
|
+
key={instructor.id}
|
|
3354
|
+
value={`${instructor.name}-${instructor.id}`}
|
|
3355
|
+
onSelect={() => {
|
|
3356
|
+
field.onChange(String(instructor.id));
|
|
3357
|
+
setInstructorOpen(false);
|
|
3358
|
+
setInstructorSearch('');
|
|
3359
|
+
}}
|
|
3360
|
+
>
|
|
3361
|
+
{instructor.name}
|
|
3362
|
+
</CommandItem>
|
|
3363
|
+
))}
|
|
3364
|
+
</CommandGroup>
|
|
3365
|
+
</CommandList>
|
|
3366
|
+
</Command>
|
|
3367
|
+
</PopoverContent>
|
|
3368
|
+
</Popover>
|
|
3369
|
+
</div>
|
|
3370
|
+
|
|
3371
|
+
<Button
|
|
3372
|
+
type="button"
|
|
3373
|
+
variant="outline"
|
|
3374
|
+
size="icon"
|
|
3375
|
+
className="shrink-0"
|
|
3376
|
+
onClick={() => setCreateInstructorDialogOpen(true)}
|
|
3377
|
+
aria-label={t(
|
|
3378
|
+
'sheet.lessonForm.fields.createInstructor'
|
|
3379
|
+
)}
|
|
3380
|
+
>
|
|
3381
|
+
<Plus className="h-4 w-4" />
|
|
3382
|
+
</Button>
|
|
3383
|
+
</div>
|
|
3384
|
+
)}
|
|
3385
|
+
/>
|
|
3386
|
+
</Field>
|
|
3387
|
+
|
|
3388
|
+
<Field>
|
|
3389
|
+
<FieldLabel>
|
|
3390
|
+
{t('sheet.lessonForm.fields.location')}
|
|
3391
|
+
</FieldLabel>
|
|
3392
|
+
<Input
|
|
3393
|
+
{...aulaForm.register('local')}
|
|
3394
|
+
placeholder={t(
|
|
3395
|
+
'sheet.lessonForm.fields.locationPlaceholder'
|
|
3396
|
+
)}
|
|
3397
|
+
/>
|
|
3398
|
+
{aulaForm.formState.errors.local && (
|
|
3399
|
+
<FieldError>
|
|
3400
|
+
{aulaForm.formState.errors.local.message}
|
|
3401
|
+
</FieldError>
|
|
3402
|
+
)}
|
|
3403
|
+
</Field>
|
|
3404
|
+
|
|
3405
|
+
<SheetFooter className="mt-6 px-0">
|
|
3406
|
+
<Button type="submit" disabled={savingAula} className="gap-2">
|
|
3407
|
+
{savingAula && <Loader2 className="size-4 animate-spin" />}
|
|
3408
|
+
{editingAula
|
|
3409
|
+
? t('sheet.lessonForm.actions.save')
|
|
3410
|
+
: t('sheet.lessonForm.actions.create')}
|
|
3411
|
+
</Button>
|
|
3412
|
+
</SheetFooter>
|
|
3413
|
+
</form>
|
|
3414
|
+
</TabsContent>
|
|
3415
|
+
|
|
3416
|
+
<TabsContent value="chamada" className="mt-4 space-y-4">
|
|
3417
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
3418
|
+
<span>
|
|
3419
|
+
{t('sheet.attendance.summary', {
|
|
3420
|
+
present: presencaList.filter(
|
|
3421
|
+
(p) => p.selecionado && p.presente
|
|
3422
|
+
).length,
|
|
3423
|
+
total: presencaList.filter((p) => p.selecionado).length,
|
|
3424
|
+
})}
|
|
3425
|
+
</span>
|
|
3426
|
+
<div className="flex gap-2">
|
|
3427
|
+
<Button
|
|
3428
|
+
variant="outline"
|
|
3429
|
+
size="sm"
|
|
3430
|
+
onClick={() =>
|
|
3431
|
+
setPresencaList((prev) =>
|
|
3432
|
+
prev.map((p) =>
|
|
3433
|
+
p.selecionado ? { ...p, presente: true } : p
|
|
3434
|
+
)
|
|
3435
|
+
)
|
|
3436
|
+
}
|
|
3437
|
+
>
|
|
3438
|
+
{t('sheet.attendance.allPresent')}
|
|
3439
|
+
</Button>
|
|
3440
|
+
<Button
|
|
3441
|
+
variant="outline"
|
|
3442
|
+
size="sm"
|
|
3443
|
+
onClick={() =>
|
|
3444
|
+
setPresencaList((prev) =>
|
|
3445
|
+
prev.map((p) =>
|
|
3446
|
+
p.selecionado ? { ...p, presente: false } : p
|
|
3447
|
+
)
|
|
3448
|
+
)
|
|
3449
|
+
}
|
|
3450
|
+
>
|
|
3451
|
+
{t('sheet.attendance.allAbsent')}
|
|
3452
|
+
</Button>
|
|
1385
3453
|
</div>
|
|
1386
|
-
|
|
1387
|
-
})}
|
|
1388
|
-
</div>
|
|
3454
|
+
</div>
|
|
1389
3455
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
3456
|
+
<div className="space-y-2">
|
|
3457
|
+
{alunos.map((aluno) => {
|
|
3458
|
+
const presenca = presencaList.find(
|
|
3459
|
+
(p) => p.alunoId === aluno.id
|
|
3460
|
+
);
|
|
3461
|
+
if (!presenca) return null;
|
|
3462
|
+
return (
|
|
3463
|
+
<div
|
|
3464
|
+
key={aluno.id}
|
|
3465
|
+
className={`flex items-center justify-between rounded-lg border p-3 transition-colors ${presenca.selecionado ? (presenca.presente ? 'bg-emerald-50/50 border-emerald-200' : 'bg-red-50/50 border-red-200') : 'bg-muted/30 border-border/60'}`}
|
|
3466
|
+
>
|
|
3467
|
+
<div className="flex items-center gap-3">
|
|
3468
|
+
<Checkbox
|
|
3469
|
+
checked={presenca.selecionado}
|
|
3470
|
+
onCheckedChange={() => toggleParticipante(aluno.id)}
|
|
3471
|
+
aria-label={t('sheet.attendance.participantLabel')}
|
|
3472
|
+
/>
|
|
3473
|
+
<Avatar className="size-9">
|
|
3474
|
+
<AvatarFallback className="text-xs">
|
|
3475
|
+
{aluno.nome
|
|
3476
|
+
.split(' ')
|
|
3477
|
+
.map((n) => n[0])
|
|
3478
|
+
.join('')
|
|
3479
|
+
.slice(0, 2)}
|
|
3480
|
+
</AvatarFallback>
|
|
3481
|
+
</Avatar>
|
|
3482
|
+
<span className="font-medium">{aluno.nome}</span>
|
|
3483
|
+
</div>
|
|
3484
|
+
<div className="flex items-center gap-3">
|
|
3485
|
+
<span
|
|
3486
|
+
className={`text-sm font-medium ${presenca.selecionado ? (presenca.presente ? 'text-emerald-600' : 'text-red-600') : 'text-muted-foreground'}`}
|
|
3487
|
+
>
|
|
3488
|
+
{presenca.selecionado
|
|
3489
|
+
? presenca.presente
|
|
3490
|
+
? t('sheet.attendance.present')
|
|
3491
|
+
: t('sheet.attendance.absent')
|
|
3492
|
+
: t('sheet.attendance.notParticipant')}
|
|
3493
|
+
</span>
|
|
3494
|
+
<Switch
|
|
3495
|
+
checked={presenca.presente}
|
|
3496
|
+
disabled={!presenca.selecionado}
|
|
3497
|
+
onCheckedChange={() => togglePresenca(aluno.id)}
|
|
3498
|
+
/>
|
|
3499
|
+
</div>
|
|
3500
|
+
</div>
|
|
3501
|
+
);
|
|
3502
|
+
})}
|
|
3503
|
+
</div>
|
|
3504
|
+
|
|
3505
|
+
<SheetFooter className="mt-6 px-0">
|
|
3506
|
+
<Button
|
|
3507
|
+
onClick={handleSaveAttendanceOnly}
|
|
3508
|
+
disabled={savingAula || savingPresenca}
|
|
3509
|
+
className="gap-2"
|
|
3510
|
+
>
|
|
3511
|
+
{savingAula || savingPresenca ? (
|
|
3512
|
+
<Loader2 className="size-4 animate-spin" />
|
|
3513
|
+
) : (
|
|
3514
|
+
<Save className="size-4" />
|
|
3515
|
+
)}
|
|
3516
|
+
{t('sheet.attendance.save')}
|
|
3517
|
+
</Button>
|
|
3518
|
+
</SheetFooter>
|
|
3519
|
+
</TabsContent>
|
|
3520
|
+
</Tabs>
|
|
1404
3521
|
</SheetContent>
|
|
1405
3522
|
</Sheet>
|
|
3523
|
+
|
|
3524
|
+
<CreateLmsPersonSheet
|
|
3525
|
+
open={createInstructorDialogOpen}
|
|
3526
|
+
onOpenChange={setCreateInstructorDialogOpen}
|
|
3527
|
+
onCreated={handleInstructorCreated}
|
|
3528
|
+
title={t('sheet.lessonForm.createInstructorTitle')}
|
|
3529
|
+
description={t('sheet.lessonForm.createInstructorDescription')}
|
|
3530
|
+
errorMessage={t('sheet.lessonForm.createInstructorError')}
|
|
3531
|
+
defaultQualificationSlugs={['class-sessions']}
|
|
3532
|
+
/>
|
|
3533
|
+
|
|
3534
|
+
<ClassFormSheet
|
|
3535
|
+
open={editSheetOpen}
|
|
3536
|
+
onOpenChange={setEditSheetOpen}
|
|
3537
|
+
classId={id}
|
|
3538
|
+
onSaved={async () => {
|
|
3539
|
+
await Promise.all([refetchTurma(), refetchAulas(), refetchAlunos()]);
|
|
3540
|
+
notifyLmsDataUpdated();
|
|
3541
|
+
}}
|
|
3542
|
+
/>
|
|
1406
3543
|
</Page>
|
|
1407
3544
|
);
|
|
1408
3545
|
}
|