From 11a726546007c5b9dee4f5db6770879bdce6e203 Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Fri, 10 Apr 2026 17:26:42 +0400 Subject: [PATCH] feat(v3): restructure project + add complete frontend - Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor --- .dockerignore | 10 + .env.example | 3 + .gitignore | 33 + Dockerfile | 41 + README.md | 139 + bun.lock | 1222 +++ bun.lockb | Bin 0 -> 245395 bytes components.json | 20 + docker-compose.yml | 9 + docs/04-AI-Stack-Report.md | 633 ++ docs/ARCHITECTURE.md | 344 + docs/DEVELOPER_FEEDBACK.md | 249 + docs/ENCOACH_ODOO19_BACKEND_SRS.md | 1959 ++++ docs/ENCOACH_PRODUCT_DESCRIPTION.md | 186 + docs/ENCOACH_SYSTEM_FEATURES_GUIDE.md | 308 + docs/ENCOACH_UNIFIED_SRS.md | 3223 +++++++ docs/MATH_IT_ADAPTIVE_LEARNING_SRS.md | 1412 +++ docs/ODOO_BACKEND_SRS_v3.md | 749 ++ docs/ODOO_DEVELOPER_HANDOFF.md | 100 + docs/ODOO_MIGRATION_FRONTEND_SRS.md | 910 ++ docs/ODOO_MIGRATION_SRS.md | 2337 +++++ docs/ODOO_MIGRATION_SRS_v2.md | 1294 +++ docs/UTAS_MASTER_PLAN.md | 727 ++ docs/UTAS_SYSTEM_SCOPE.md | 86 + eslint.config.js | 26 + index.html | 20 + package-lock.json | 8333 +++++++++++++++++ package.json | 89 + postcss.config.js | 6 + public/favicon.ico | Bin 0 -> 20373 bytes public/logo.png | Bin 0 -> 49453 bytes public/placeholder.svg | 1 + public/robots.txt | 14 + public/templates/students.csv | 2 + src/App.css | 42 + src/App.tsx | 345 + src/components/AdminLmsLayout.tsx | 354 + src/components/AppLayout.tsx | 145 + src/components/AppSidebar.tsx | 158 + src/components/NavLink.tsx | 28 + src/components/NotificationDropdown.tsx | 36 + src/components/ProtectedRoute.tsx | 64 + src/components/RoleLayout.tsx | 151 + src/components/StudentLayout.tsx | 51 + src/components/TeacherLayout.tsx | 42 + src/components/ai/AiAlertBanner.tsx | 80 + src/components/ai/AiAssistantDrawer.tsx | 139 + src/components/ai/AiBatchOptimizer.tsx | 101 + src/components/ai/AiCreationAssistant.tsx | 215 + src/components/ai/AiGeneratorModal.tsx | 207 + src/components/ai/AiGradeExplainer.tsx | 78 + src/components/ai/AiGradingAssistant.tsx | 100 + src/components/ai/AiInsightsPanel.tsx | 105 + src/components/ai/AiReportNarrative.tsx | 56 + src/components/ai/AiRiskBadge.tsx | 45 + src/components/ai/AiSearchBar.tsx | 103 + src/components/ai/AiStudyCoach.tsx | 102 + src/components/ai/AiTipBanner.tsx | 81 + src/components/ai/AiWritingHelper.tsx | 140 + src/components/ui/accordion.tsx | 52 + src/components/ui/alert-dialog.tsx | 104 + src/components/ui/alert.tsx | 43 + src/components/ui/aspect-ratio.tsx | 5 + src/components/ui/avatar.tsx | 38 + src/components/ui/badge.tsx | 29 + src/components/ui/breadcrumb.tsx | 90 + src/components/ui/button.tsx | 47 + src/components/ui/calendar.tsx | 54 + src/components/ui/card.tsx | 43 + src/components/ui/carousel.tsx | 224 + src/components/ui/chart.tsx | 303 + src/components/ui/checkbox.tsx | 26 + src/components/ui/collapsible.tsx | 9 + src/components/ui/command.tsx | 132 + src/components/ui/context-menu.tsx | 178 + src/components/ui/dialog.tsx | 95 + src/components/ui/drawer.tsx | 87 + src/components/ui/dropdown-menu.tsx | 179 + src/components/ui/form.tsx | 129 + src/components/ui/hover-card.tsx | 27 + src/components/ui/input-otp.tsx | 61 + src/components/ui/input.tsx | 22 + src/components/ui/label.tsx | 17 + src/components/ui/menubar.tsx | 207 + src/components/ui/navigation-menu.tsx | 120 + src/components/ui/pagination.tsx | 81 + src/components/ui/popover.tsx | 29 + src/components/ui/progress.tsx | 23 + src/components/ui/radio-group.tsx | 36 + src/components/ui/resizable.tsx | 37 + src/components/ui/scroll-area.tsx | 38 + src/components/ui/select.tsx | 143 + src/components/ui/separator.tsx | 20 + src/components/ui/sheet.tsx | 107 + src/components/ui/sidebar.tsx | 637 ++ src/components/ui/skeleton.tsx | 7 + src/components/ui/slider.tsx | 23 + src/components/ui/sonner.tsx | 27 + src/components/ui/switch.tsx | 27 + src/components/ui/table.tsx | 72 + src/components/ui/tabs.tsx | 53 + src/components/ui/textarea.tsx | 21 + src/components/ui/toast.tsx | 111 + src/components/ui/toaster.tsx | 24 + src/components/ui/toggle-group.tsx | 49 + src/components/ui/toggle.tsx | 37 + src/components/ui/tooltip.tsx | 28 + src/components/ui/use-toast.ts | 3 + src/contexts/AuthContext.tsx | 68 + src/hooks/queries/index.ts | 28 + src/hooks/queries/keys.ts | 304 + src/hooks/queries/useAcademic.ts | 104 + src/hooks/queries/useActivities.ts | 60 + src/hooks/queries/useAdaptive.ts | 81 + src/hooks/queries/useAdaptiveEngine.ts | 51 + src/hooks/queries/useAdmissions.ts | 94 + src/hooks/queries/useAiCourse.ts | 97 + src/hooks/queries/useAssignments.ts | 27 + src/hooks/queries/useCommunication.ts | 140 + src/hooks/queries/useCourseGeneration.ts | 48 + src/hooks/queries/useCourseware.ts | 204 + src/hooks/queries/useEntities.ts | 27 + src/hooks/queries/useEntityOnboarding.ts | 57 + src/hooks/queries/useExamSession.ts | 34 + src/hooks/queries/useExamTemplates.ts | 85 + src/hooks/queries/useExams.ts | 48 + src/hooks/queries/useFacilities.ts | 60 + src/hooks/queries/useFaq.ts | 85 + src/hooks/queries/useFees.ts | 33 + src/hooks/queries/useGradebook.ts | 52 + src/hooks/queries/useGrading.ts | 42 + src/hooks/queries/useInstitutionalExams.ts | 199 + src/hooks/queries/useLessons.ts | 37 + src/hooks/queries/useLibrary.ts | 79 + src/hooks/queries/useLms.ts | 97 + src/hooks/queries/useNotifications.ts | 103 + src/hooks/queries/usePlacement.ts | 54 + src/hooks/queries/useScoreRelease.ts | 41 + src/hooks/queries/useSignup.ts | 41 + src/hooks/queries/useStudentLeave.ts | 60 + src/hooks/queries/useStudentProgress.ts | 11 + src/hooks/queries/useUsers.ts | 18 + src/hooks/use-mobile.tsx | 19 + src/hooks/use-toast.ts | 186 + src/hooks/usePermissions.ts | 54 + src/index.css | 122 + src/lib/api-client.ts | 140 + src/lib/odoo-api.ts | 26 + src/lib/query-client.ts | 19 + src/lib/utils.ts | 6 + src/main.tsx | 5 + src/pages/AdminDashboard.tsx | 216 + src/pages/AdmissionApplication.tsx | 205 + src/pages/ApprovalWorkflowsPage.tsx | 119 + src/pages/AssignmentsPage.tsx | 118 + src/pages/ClassroomsPage.tsx | 133 + src/pages/EmailVerification.tsx | 191 + src/pages/EntitiesPage.tsx | 95 + src/pages/ExamPage.tsx | 54 + src/pages/ExamStructuresPage.tsx | 85 + src/pages/ExamsListPage.tsx | 76 + src/pages/FaqPage.tsx | 96 + src/pages/ForgotPassword.tsx | 59 + src/pages/GenerationPage.tsx | 95 + src/pages/GrammarPage.tsx | 94 + src/pages/Index.tsx | 14 + src/pages/Login.tsx | 141 + src/pages/NotFound.tsx | 24 + src/pages/OfficialExamAccess.tsx | 88 + src/pages/OnboardingWizard.tsx | 572 ++ src/pages/PaymentRecordPage.tsx | 120 + src/pages/ProfilePage.tsx | 75 + src/pages/RecordPage.tsx | 68 + src/pages/Register.tsx | 289 + src/pages/ResetPassword.tsx | 147 + src/pages/RubricsPage.tsx | 122 + src/pages/ScoreVerification.tsx | 50 + src/pages/SettingsPage.tsx | 103 + src/pages/StatsCorporatePage.tsx | 133 + src/pages/StudentPerformancePage.tsx | 79 + src/pages/TicketsPage.tsx | 164 + src/pages/UsersPage.tsx | 335 + src/pages/VocabularyPage.tsx | 96 + src/pages/admin/AcademicYearManager.tsx | 209 + src/pages/admin/AdaptiveDashboard.tsx | 158 + src/pages/admin/AdaptiveStudentDetail.tsx | 105 + src/pages/admin/AdminActivities.tsx | 281 + src/pages/admin/AdminBatchDetail.tsx | 89 + src/pages/admin/AdminBatches.tsx | 95 + src/pages/admin/AdminCourses.tsx | 109 + src/pages/admin/AdminFacilities.tsx | 251 + src/pages/admin/AdminFees.tsx | 167 + src/pages/admin/AdminGradebook.tsx | 219 + src/pages/admin/AdminLessons.tsx | 269 + src/pages/admin/AdminLibrary.tsx | 328 + src/pages/admin/AdminLmsDashboard.tsx | 202 + src/pages/admin/AdminProfile.tsx | 69 + src/pages/admin/AdminReports.tsx | 97 + src/pages/admin/AdminSettings.tsx | 80 + src/pages/admin/AdminStudentLeave.tsx | 252 + src/pages/admin/AdminStudentProgress.tsx | 70 + src/pages/admin/AdminStudents.tsx | 334 + src/pages/admin/AdminTeachers.tsx | 178 + src/pages/admin/AdminTimetable.tsx | 146 + src/pages/admin/AdmissionDetail.tsx | 173 + src/pages/admin/AdmissionList.tsx | 111 + src/pages/admin/AdmissionRegisterPage.tsx | 182 + src/pages/admin/AiEnglishQuality.tsx | 136 + src/pages/admin/AiEnglishTaxonomy.tsx | 108 + src/pages/admin/AiIeltsValidation.tsx | 207 + src/pages/admin/ApprovalWorkflowConfig.tsx | 222 + src/pages/admin/AuthorityMatrix.tsx | 348 + src/pages/admin/BulkStudentUpload.tsx | 218 + src/pages/admin/CourseConfig.tsx | 367 + src/pages/admin/CredentialDashboard.tsx | 206 + src/pages/admin/CustomExamCreate.tsx | 597 ++ src/pages/admin/DepartmentManager.tsx | 155 + src/pages/admin/ExamTemplateSelection.tsx | 209 + src/pages/admin/FaqManager.tsx | 285 + src/pages/admin/GradingQueue.tsx | 227 + src/pages/admin/IeltsContentPool.tsx | 353 + src/pages/admin/IeltsExamCreate.tsx | 426 + src/pages/admin/IeltsExamValidation.tsx | 246 + src/pages/admin/IeltsSkillConfig.tsx | 210 + src/pages/admin/InstitutionalExamSessions.tsx | 274 + src/pages/admin/LevelMappingConfig.tsx | 170 + src/pages/admin/MarksheetManager.tsx | 312 + src/pages/admin/ModuleBuilder.tsx | 335 + src/pages/admin/NotificationRules.tsx | 196 + src/pages/admin/ResourceManager.tsx | 187 + src/pages/admin/RolesPermissions.tsx | 381 + src/pages/admin/ScoreApprovalQueue.tsx | 193 + src/pages/admin/TaxonomyManager.tsx | 238 + src/pages/admin/UserRoles.tsx | 383 + src/pages/admin/WhiteLabelBranding.tsx | 285 + src/pages/student/AiEnglishCourse.tsx | 291 + src/pages/student/AiIeltsCourse.tsx | 317 + src/pages/student/CourseDelivery.tsx | 204 + src/pages/student/DiagnosticTest.tsx | 127 + src/pages/student/ExamResults.tsx | 178 + src/pages/student/ExamSession.tsx | 458 + src/pages/student/ExamStatus.tsx | 111 + src/pages/student/GapAnalysis.tsx | 210 + src/pages/student/LearningPlan.tsx | 122 + src/pages/student/PlacementAccess.tsx | 135 + src/pages/student/PlacementBriefing.tsx | 132 + src/pages/student/PlacementResults.tsx | 281 + src/pages/student/PlacementTest.tsx | 364 + src/pages/student/ProficiencyProfile.tsx | 117 + src/pages/student/StudentAnnouncements.tsx | 65 + src/pages/student/StudentAssignments.tsx | 93 + src/pages/student/StudentAttendance.tsx | 75 + src/pages/student/StudentChapterView.tsx | 126 + src/pages/student/StudentCourseDetail.tsx | 126 + src/pages/student/StudentCourses.tsx | 58 + src/pages/student/StudentDashboard.tsx | 115 + src/pages/student/StudentDiscussionBoard.tsx | 176 + src/pages/student/StudentGrades.tsx | 86 + src/pages/student/StudentJourney.tsx | 111 + src/pages/student/StudentMessages.tsx | 171 + src/pages/student/StudentProfile.tsx | 61 + src/pages/student/StudentTimetable.tsx | 74 + src/pages/student/SubjectRegistrationPage.tsx | 145 + src/pages/student/SubjectSelection.tsx | 86 + src/pages/student/TopicLearning.tsx | 126 + src/pages/teacher/AdaptiveSettings.tsx | 198 + src/pages/teacher/AiWorkbench.tsx | 201 + src/pages/teacher/ChapterDetail.tsx | 200 + src/pages/teacher/CourseBuilder.tsx | 119 + src/pages/teacher/CourseChapters.tsx | 163 + src/pages/teacher/CourseProgress.tsx | 200 + src/pages/teacher/TeacherAnnouncements.tsx | 155 + src/pages/teacher/TeacherAssignmentDetail.tsx | 89 + src/pages/teacher/TeacherAssignments.tsx | 83 + src/pages/teacher/TeacherAttendance.tsx | 126 + src/pages/teacher/TeacherCourses.tsx | 52 + src/pages/teacher/TeacherDashboard.tsx | 107 + src/pages/teacher/TeacherDiscussionBoard.tsx | 198 + src/pages/teacher/TeacherProfile.tsx | 49 + src/pages/teacher/TeacherStudents.tsx | 76 + src/pages/teacher/TeacherTimetable.tsx | 63 + src/services/academic.service.ts | 64 + src/services/activity.service.ts | 33 + src/services/adaptive-engine.service.ts | 30 + src/services/adaptive.service.ts | 78 + src/services/admission.service.ts | 56 + src/services/ai-course.service.ts | 41 + src/services/analytics.service.ts | 44 + src/services/approvals.service.ts | 30 + src/services/assignments.service.ts | 37 + src/services/auth.service.ts | 30 + src/services/branding.service.ts | 29 + src/services/classrooms.service.ts | 36 + src/services/coaching.service.ts | 28 + src/services/communication.service.ts | 96 + src/services/course-generation.service.ts | 37 + src/services/courseware.service.ts | 113 + src/services/custom-exam.service.ts | 17 + src/services/entities.service.ts | 45 + src/services/entity-onboarding.service.ts | 36 + src/services/evaluations.service.ts | 20 + src/services/exam-session.service.ts | 17 + src/services/exam-template.service.ts | 17 + src/services/exams.service.ts | 62 + src/services/facility.service.ts | 33 + src/services/faq.service.ts | 42 + src/services/fees.service.ts | 21 + src/services/generation.service.ts | 22 + src/services/gradebook.service.ts | 29 + src/services/grading.service.ts | 26 + src/services/ielts-exam.service.ts | 44 + src/services/index.ts | 37 + src/services/institutional-exam.service.ts | 167 + src/services/lesson.service.ts | 21 + src/services/level-mapping.service.ts | 13 + src/services/library.service.ts | 41 + src/services/lms.service.ts | 225 + src/services/media.service.ts | 15 + src/services/notification.service.ts | 55 + src/services/placement.service.ts | 42 + src/services/plagiarism.service.ts | 29 + src/services/report.service.ts | 19 + src/services/resources.service.ts | 48 + src/services/roles.service.ts | 75 + src/services/score-release.service.ts | 17 + src/services/signup.service.ts | 31 + src/services/stats.service.ts | 20 + src/services/storage.service.ts | 11 + src/services/student-leave.service.ts | 33 + src/services/student-progress.service.ts | 9 + src/services/subscriptions.service.ts | 36 + src/services/taxonomy.service.ts | 73 + src/services/tickets.service.ts | 27 + src/services/training.service.ts | 16 + src/services/users.service.ts | 59 + src/services/verification.service.ts | 6 + src/test/example.test.ts | 7 + src/test/setup.ts | 15 + src/types/academic.ts | 50 + src/types/activity.ts | 3 + src/types/adaptive.ts | 164 + src/types/admission.ts | 63 + src/types/ai-course.ts | 86 + src/types/ai.ts | 91 + src/types/assignment.ts | 37 + src/types/auth.ts | 54 + src/types/branding.ts | 15 + src/types/classroom.ts | 24 + src/types/common.ts | 27 + src/types/communication.ts | 91 + src/types/course-generation.ts | 84 + src/types/courseware.ts | 87 + src/types/custom-exam.ts | 44 + src/types/entity-onboarding.ts | 52 + src/types/entity.ts | 64 + src/types/exam-session.ts | 59 + src/types/exam-template.ts | 53 + src/types/exam.ts | 49 + src/types/facility.ts | 4 + src/types/faq.ts | 38 + src/types/fees.ts | 27 + src/types/gradebook.ts | 30 + src/types/grading.ts | 38 + src/types/ielts-exam.ts | 84 + src/types/index.ts | 45 + src/types/institutional-exam.ts | 189 + src/types/it.ts | 26 + src/types/lesson.ts | 19 + src/types/level-mapping.ts | 12 + src/types/library.ts | 15 + src/types/lms.ts | 158 + src/types/math.ts | 31 + src/types/notification.ts | 48 + src/types/placement.ts | 75 + src/types/role-permission.ts | 81 + src/types/score-release.ts | 16 + src/types/signup.ts | 52 + src/types/stats.ts | 44 + src/types/student-leave.ts | 22 + src/types/student-progress.ts | 8 + src/types/subscription.ts | 36 + src/types/taxonomy.ts | 58 + src/types/ticket.ts | 27 + src/types/training.ts | 28 + src/types/verification.ts | 7 + src/vite-env.d.ts | 10 + tailwind.config.ts | 100 + tsconfig.app.json | 35 + tsconfig.json | 24 + tsconfig.node.json | 22 + vite.config.ts | 28 + vitest.config.ts | 16 + 392 files changed, 62287 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bun.lock create mode 100755 bun.lockb create mode 100644 components.json create mode 100644 docker-compose.yml create mode 100644 docs/04-AI-Stack-Report.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DEVELOPER_FEEDBACK.md create mode 100644 docs/ENCOACH_ODOO19_BACKEND_SRS.md create mode 100644 docs/ENCOACH_PRODUCT_DESCRIPTION.md create mode 100644 docs/ENCOACH_SYSTEM_FEATURES_GUIDE.md create mode 100644 docs/ENCOACH_UNIFIED_SRS.md create mode 100644 docs/MATH_IT_ADAPTIVE_LEARNING_SRS.md create mode 100644 docs/ODOO_BACKEND_SRS_v3.md create mode 100644 docs/ODOO_DEVELOPER_HANDOFF.md create mode 100644 docs/ODOO_MIGRATION_FRONTEND_SRS.md create mode 100644 docs/ODOO_MIGRATION_SRS.md create mode 100644 docs/ODOO_MIGRATION_SRS_v2.md create mode 100644 docs/UTAS_MASTER_PLAN.md create mode 100644 docs/UTAS_SYSTEM_SCOPE.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/favicon.ico create mode 100644 public/logo.png create mode 100644 public/placeholder.svg create mode 100644 public/robots.txt create mode 100644 public/templates/students.csv create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/components/AdminLmsLayout.tsx create mode 100644 src/components/AppLayout.tsx create mode 100644 src/components/AppSidebar.tsx create mode 100644 src/components/NavLink.tsx create mode 100644 src/components/NotificationDropdown.tsx create mode 100644 src/components/ProtectedRoute.tsx create mode 100644 src/components/RoleLayout.tsx create mode 100644 src/components/StudentLayout.tsx create mode 100644 src/components/TeacherLayout.tsx create mode 100644 src/components/ai/AiAlertBanner.tsx create mode 100644 src/components/ai/AiAssistantDrawer.tsx create mode 100644 src/components/ai/AiBatchOptimizer.tsx create mode 100644 src/components/ai/AiCreationAssistant.tsx create mode 100644 src/components/ai/AiGeneratorModal.tsx create mode 100644 src/components/ai/AiGradeExplainer.tsx create mode 100644 src/components/ai/AiGradingAssistant.tsx create mode 100644 src/components/ai/AiInsightsPanel.tsx create mode 100644 src/components/ai/AiReportNarrative.tsx create mode 100644 src/components/ai/AiRiskBadge.tsx create mode 100644 src/components/ai/AiSearchBar.tsx create mode 100644 src/components/ai/AiStudyCoach.tsx create mode 100644 src/components/ai/AiTipBanner.tsx create mode 100644 src/components/ai/AiWritingHelper.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/aspect-ratio.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/breadcrumb.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/carousel.tsx create mode 100644 src/components/ui/chart.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/context-menu.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/hover-card.tsx create mode 100644 src/components/ui/input-otp.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/menubar.tsx create mode 100644 src/components/ui/navigation-menu.tsx create mode 100644 src/components/ui/pagination.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/components/ui/resizable.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/sidebar.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/toggle-group.tsx create mode 100644 src/components/ui/toggle.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/ui/use-toast.ts create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/hooks/queries/index.ts create mode 100644 src/hooks/queries/keys.ts create mode 100644 src/hooks/queries/useAcademic.ts create mode 100644 src/hooks/queries/useActivities.ts create mode 100644 src/hooks/queries/useAdaptive.ts create mode 100644 src/hooks/queries/useAdaptiveEngine.ts create mode 100644 src/hooks/queries/useAdmissions.ts create mode 100644 src/hooks/queries/useAiCourse.ts create mode 100644 src/hooks/queries/useAssignments.ts create mode 100644 src/hooks/queries/useCommunication.ts create mode 100644 src/hooks/queries/useCourseGeneration.ts create mode 100644 src/hooks/queries/useCourseware.ts create mode 100644 src/hooks/queries/useEntities.ts create mode 100644 src/hooks/queries/useEntityOnboarding.ts create mode 100644 src/hooks/queries/useExamSession.ts create mode 100644 src/hooks/queries/useExamTemplates.ts create mode 100644 src/hooks/queries/useExams.ts create mode 100644 src/hooks/queries/useFacilities.ts create mode 100644 src/hooks/queries/useFaq.ts create mode 100644 src/hooks/queries/useFees.ts create mode 100644 src/hooks/queries/useGradebook.ts create mode 100644 src/hooks/queries/useGrading.ts create mode 100644 src/hooks/queries/useInstitutionalExams.ts create mode 100644 src/hooks/queries/useLessons.ts create mode 100644 src/hooks/queries/useLibrary.ts create mode 100644 src/hooks/queries/useLms.ts create mode 100644 src/hooks/queries/useNotifications.ts create mode 100644 src/hooks/queries/usePlacement.ts create mode 100644 src/hooks/queries/useScoreRelease.ts create mode 100644 src/hooks/queries/useSignup.ts create mode 100644 src/hooks/queries/useStudentLeave.ts create mode 100644 src/hooks/queries/useStudentProgress.ts create mode 100644 src/hooks/queries/useUsers.ts create mode 100644 src/hooks/use-mobile.tsx create mode 100644 src/hooks/use-toast.ts create mode 100644 src/hooks/usePermissions.ts create mode 100644 src/index.css create mode 100644 src/lib/api-client.ts create mode 100644 src/lib/odoo-api.ts create mode 100644 src/lib/query-client.ts create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/pages/AdminDashboard.tsx create mode 100644 src/pages/AdmissionApplication.tsx create mode 100644 src/pages/ApprovalWorkflowsPage.tsx create mode 100644 src/pages/AssignmentsPage.tsx create mode 100644 src/pages/ClassroomsPage.tsx create mode 100644 src/pages/EmailVerification.tsx create mode 100644 src/pages/EntitiesPage.tsx create mode 100644 src/pages/ExamPage.tsx create mode 100644 src/pages/ExamStructuresPage.tsx create mode 100644 src/pages/ExamsListPage.tsx create mode 100644 src/pages/FaqPage.tsx create mode 100644 src/pages/ForgotPassword.tsx create mode 100644 src/pages/GenerationPage.tsx create mode 100644 src/pages/GrammarPage.tsx create mode 100644 src/pages/Index.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/NotFound.tsx create mode 100644 src/pages/OfficialExamAccess.tsx create mode 100644 src/pages/OnboardingWizard.tsx create mode 100644 src/pages/PaymentRecordPage.tsx create mode 100644 src/pages/ProfilePage.tsx create mode 100644 src/pages/RecordPage.tsx create mode 100644 src/pages/Register.tsx create mode 100644 src/pages/ResetPassword.tsx create mode 100644 src/pages/RubricsPage.tsx create mode 100644 src/pages/ScoreVerification.tsx create mode 100644 src/pages/SettingsPage.tsx create mode 100644 src/pages/StatsCorporatePage.tsx create mode 100644 src/pages/StudentPerformancePage.tsx create mode 100644 src/pages/TicketsPage.tsx create mode 100644 src/pages/UsersPage.tsx create mode 100644 src/pages/VocabularyPage.tsx create mode 100644 src/pages/admin/AcademicYearManager.tsx create mode 100644 src/pages/admin/AdaptiveDashboard.tsx create mode 100644 src/pages/admin/AdaptiveStudentDetail.tsx create mode 100644 src/pages/admin/AdminActivities.tsx create mode 100644 src/pages/admin/AdminBatchDetail.tsx create mode 100644 src/pages/admin/AdminBatches.tsx create mode 100644 src/pages/admin/AdminCourses.tsx create mode 100644 src/pages/admin/AdminFacilities.tsx create mode 100644 src/pages/admin/AdminFees.tsx create mode 100644 src/pages/admin/AdminGradebook.tsx create mode 100644 src/pages/admin/AdminLessons.tsx create mode 100644 src/pages/admin/AdminLibrary.tsx create mode 100644 src/pages/admin/AdminLmsDashboard.tsx create mode 100644 src/pages/admin/AdminProfile.tsx create mode 100644 src/pages/admin/AdminReports.tsx create mode 100644 src/pages/admin/AdminSettings.tsx create mode 100644 src/pages/admin/AdminStudentLeave.tsx create mode 100644 src/pages/admin/AdminStudentProgress.tsx create mode 100644 src/pages/admin/AdminStudents.tsx create mode 100644 src/pages/admin/AdminTeachers.tsx create mode 100644 src/pages/admin/AdminTimetable.tsx create mode 100644 src/pages/admin/AdmissionDetail.tsx create mode 100644 src/pages/admin/AdmissionList.tsx create mode 100644 src/pages/admin/AdmissionRegisterPage.tsx create mode 100644 src/pages/admin/AiEnglishQuality.tsx create mode 100644 src/pages/admin/AiEnglishTaxonomy.tsx create mode 100644 src/pages/admin/AiIeltsValidation.tsx create mode 100644 src/pages/admin/ApprovalWorkflowConfig.tsx create mode 100644 src/pages/admin/AuthorityMatrix.tsx create mode 100644 src/pages/admin/BulkStudentUpload.tsx create mode 100644 src/pages/admin/CourseConfig.tsx create mode 100644 src/pages/admin/CredentialDashboard.tsx create mode 100644 src/pages/admin/CustomExamCreate.tsx create mode 100644 src/pages/admin/DepartmentManager.tsx create mode 100644 src/pages/admin/ExamTemplateSelection.tsx create mode 100644 src/pages/admin/FaqManager.tsx create mode 100644 src/pages/admin/GradingQueue.tsx create mode 100644 src/pages/admin/IeltsContentPool.tsx create mode 100644 src/pages/admin/IeltsExamCreate.tsx create mode 100644 src/pages/admin/IeltsExamValidation.tsx create mode 100644 src/pages/admin/IeltsSkillConfig.tsx create mode 100644 src/pages/admin/InstitutionalExamSessions.tsx create mode 100644 src/pages/admin/LevelMappingConfig.tsx create mode 100644 src/pages/admin/MarksheetManager.tsx create mode 100644 src/pages/admin/ModuleBuilder.tsx create mode 100644 src/pages/admin/NotificationRules.tsx create mode 100644 src/pages/admin/ResourceManager.tsx create mode 100644 src/pages/admin/RolesPermissions.tsx create mode 100644 src/pages/admin/ScoreApprovalQueue.tsx create mode 100644 src/pages/admin/TaxonomyManager.tsx create mode 100644 src/pages/admin/UserRoles.tsx create mode 100644 src/pages/admin/WhiteLabelBranding.tsx create mode 100644 src/pages/student/AiEnglishCourse.tsx create mode 100644 src/pages/student/AiIeltsCourse.tsx create mode 100644 src/pages/student/CourseDelivery.tsx create mode 100644 src/pages/student/DiagnosticTest.tsx create mode 100644 src/pages/student/ExamResults.tsx create mode 100644 src/pages/student/ExamSession.tsx create mode 100644 src/pages/student/ExamStatus.tsx create mode 100644 src/pages/student/GapAnalysis.tsx create mode 100644 src/pages/student/LearningPlan.tsx create mode 100644 src/pages/student/PlacementAccess.tsx create mode 100644 src/pages/student/PlacementBriefing.tsx create mode 100644 src/pages/student/PlacementResults.tsx create mode 100644 src/pages/student/PlacementTest.tsx create mode 100644 src/pages/student/ProficiencyProfile.tsx create mode 100644 src/pages/student/StudentAnnouncements.tsx create mode 100644 src/pages/student/StudentAssignments.tsx create mode 100644 src/pages/student/StudentAttendance.tsx create mode 100644 src/pages/student/StudentChapterView.tsx create mode 100644 src/pages/student/StudentCourseDetail.tsx create mode 100644 src/pages/student/StudentCourses.tsx create mode 100644 src/pages/student/StudentDashboard.tsx create mode 100644 src/pages/student/StudentDiscussionBoard.tsx create mode 100644 src/pages/student/StudentGrades.tsx create mode 100644 src/pages/student/StudentJourney.tsx create mode 100644 src/pages/student/StudentMessages.tsx create mode 100644 src/pages/student/StudentProfile.tsx create mode 100644 src/pages/student/StudentTimetable.tsx create mode 100644 src/pages/student/SubjectRegistrationPage.tsx create mode 100644 src/pages/student/SubjectSelection.tsx create mode 100644 src/pages/student/TopicLearning.tsx create mode 100644 src/pages/teacher/AdaptiveSettings.tsx create mode 100644 src/pages/teacher/AiWorkbench.tsx create mode 100644 src/pages/teacher/ChapterDetail.tsx create mode 100644 src/pages/teacher/CourseBuilder.tsx create mode 100644 src/pages/teacher/CourseChapters.tsx create mode 100644 src/pages/teacher/CourseProgress.tsx create mode 100644 src/pages/teacher/TeacherAnnouncements.tsx create mode 100644 src/pages/teacher/TeacherAssignmentDetail.tsx create mode 100644 src/pages/teacher/TeacherAssignments.tsx create mode 100644 src/pages/teacher/TeacherAttendance.tsx create mode 100644 src/pages/teacher/TeacherCourses.tsx create mode 100644 src/pages/teacher/TeacherDashboard.tsx create mode 100644 src/pages/teacher/TeacherDiscussionBoard.tsx create mode 100644 src/pages/teacher/TeacherProfile.tsx create mode 100644 src/pages/teacher/TeacherStudents.tsx create mode 100644 src/pages/teacher/TeacherTimetable.tsx create mode 100644 src/services/academic.service.ts create mode 100644 src/services/activity.service.ts create mode 100644 src/services/adaptive-engine.service.ts create mode 100644 src/services/adaptive.service.ts create mode 100644 src/services/admission.service.ts create mode 100644 src/services/ai-course.service.ts create mode 100644 src/services/analytics.service.ts create mode 100644 src/services/approvals.service.ts create mode 100644 src/services/assignments.service.ts create mode 100644 src/services/auth.service.ts create mode 100644 src/services/branding.service.ts create mode 100644 src/services/classrooms.service.ts create mode 100644 src/services/coaching.service.ts create mode 100644 src/services/communication.service.ts create mode 100644 src/services/course-generation.service.ts create mode 100644 src/services/courseware.service.ts create mode 100644 src/services/custom-exam.service.ts create mode 100644 src/services/entities.service.ts create mode 100644 src/services/entity-onboarding.service.ts create mode 100644 src/services/evaluations.service.ts create mode 100644 src/services/exam-session.service.ts create mode 100644 src/services/exam-template.service.ts create mode 100644 src/services/exams.service.ts create mode 100644 src/services/facility.service.ts create mode 100644 src/services/faq.service.ts create mode 100644 src/services/fees.service.ts create mode 100644 src/services/generation.service.ts create mode 100644 src/services/gradebook.service.ts create mode 100644 src/services/grading.service.ts create mode 100644 src/services/ielts-exam.service.ts create mode 100644 src/services/index.ts create mode 100644 src/services/institutional-exam.service.ts create mode 100644 src/services/lesson.service.ts create mode 100644 src/services/level-mapping.service.ts create mode 100644 src/services/library.service.ts create mode 100644 src/services/lms.service.ts create mode 100644 src/services/media.service.ts create mode 100644 src/services/notification.service.ts create mode 100644 src/services/placement.service.ts create mode 100644 src/services/plagiarism.service.ts create mode 100644 src/services/report.service.ts create mode 100644 src/services/resources.service.ts create mode 100644 src/services/roles.service.ts create mode 100644 src/services/score-release.service.ts create mode 100644 src/services/signup.service.ts create mode 100644 src/services/stats.service.ts create mode 100644 src/services/storage.service.ts create mode 100644 src/services/student-leave.service.ts create mode 100644 src/services/student-progress.service.ts create mode 100644 src/services/subscriptions.service.ts create mode 100644 src/services/taxonomy.service.ts create mode 100644 src/services/tickets.service.ts create mode 100644 src/services/training.service.ts create mode 100644 src/services/users.service.ts create mode 100644 src/services/verification.service.ts create mode 100644 src/test/example.test.ts create mode 100644 src/test/setup.ts create mode 100644 src/types/academic.ts create mode 100644 src/types/activity.ts create mode 100644 src/types/adaptive.ts create mode 100644 src/types/admission.ts create mode 100644 src/types/ai-course.ts create mode 100644 src/types/ai.ts create mode 100644 src/types/assignment.ts create mode 100644 src/types/auth.ts create mode 100644 src/types/branding.ts create mode 100644 src/types/classroom.ts create mode 100644 src/types/common.ts create mode 100644 src/types/communication.ts create mode 100644 src/types/course-generation.ts create mode 100644 src/types/courseware.ts create mode 100644 src/types/custom-exam.ts create mode 100644 src/types/entity-onboarding.ts create mode 100644 src/types/entity.ts create mode 100644 src/types/exam-session.ts create mode 100644 src/types/exam-template.ts create mode 100644 src/types/exam.ts create mode 100644 src/types/facility.ts create mode 100644 src/types/faq.ts create mode 100644 src/types/fees.ts create mode 100644 src/types/gradebook.ts create mode 100644 src/types/grading.ts create mode 100644 src/types/ielts-exam.ts create mode 100644 src/types/index.ts create mode 100644 src/types/institutional-exam.ts create mode 100644 src/types/it.ts create mode 100644 src/types/lesson.ts create mode 100644 src/types/level-mapping.ts create mode 100644 src/types/library.ts create mode 100644 src/types/lms.ts create mode 100644 src/types/math.ts create mode 100644 src/types/notification.ts create mode 100644 src/types/placement.ts create mode 100644 src/types/role-permission.ts create mode 100644 src/types/score-release.ts create mode 100644 src/types/signup.ts create mode 100644 src/types/stats.ts create mode 100644 src/types/student-leave.ts create mode 100644 src/types/student-progress.ts create mode 100644 src/types/subscription.ts create mode 100644 src/types/taxonomy.ts create mode 100644 src/types/ticket.ts create mode 100644 src/types/training.ts create mode 100644 src/types/verification.ts create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b5b764aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.env +.env.local +.env.production +*.md +docs/ +.vscode/ +coverage/ diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..f1ee21af --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Dev: use /api with Vite proxy (see vite.config.ts). Production: full URL, e.g. https://your-odoo/api +VITE_API_BASE_URL=/api +VITE_APP_NAME=EnCoach diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4899f06f --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +build/ +.next/ +out/ + +# Environment / secrets — never commit +.env +.env.* +!.env.example +.env.local +.env.production +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5545dd49 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM node:18-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +ARG VITE_API_BASE_URL=/api +ARG VITE_APP_NAME=EnCoach +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +ENV VITE_APP_NAME=$VITE_APP_NAME +RUN npm run build + +FROM nginx:stable-alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY <<'NGINX' /etc/nginx/conf.d/default.conf +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8069/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } +} +NGINX +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..d7b8349d --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# EnCoach Frontend — v2 + +## Branching Workflow + +This repo is connected to the staging server via a Git post-receive hook. +**All deployment is automatic — but only after code review approval.** + +### How to contribute + +1. Never push directly to `main` — branch protection will block it. +2. Push your changes to your feature branch (e.g. `feature/full-frontend-v1`) +3. Open a Pull Request on Gitea targeting `main` +4. Request review from **devops (Talal)** +5. Once approved and merged, the staging server rebuilds and redeploys automatically. + +### Environment + +The `.env` file is **not committed**. It lives only on the staging server at `/opt/encoach/frontend-v2/.env`. + +--- + +# EnCoach — Adaptive Learning Platform (Frontend) + +The frontend application for the EnCoach Adaptive Learning Platform, serving UTAS university students and freelance learners across IELTS, Mathematics, and IT courses. + +Built with React 18, TypeScript, and Vite. Connects to an Odoo 19 backend via REST API. + +## Tech Stack + +| Layer | Technology | +|---|---| +| Framework | React 18 + TypeScript | +| Build | Vite 5 (SWC) | +| Routing | React Router v6 | +| State / Data | TanStack Query v5, React Context | +| UI Components | shadcn/ui + Radix UI | +| Styling | Tailwind CSS 3 | +| Forms | react-hook-form + Zod validation | +| Charts | Recharts | +| Icons | Lucide React | + +## Architecture + +``` +src/ +├── components/ # Shared UI, layouts, AI components +│ ├── ai/ # 14 AI-powered components (coaching, grading, insights) +│ └── ui/ # shadcn/ui primitives +├── contexts/ # AuthContext (JWT session management) +├── hooks/ +│ ├── queries/ # TanStack Query hooks (exams, assignments, LMS, adaptive) +│ └── usePermissions # Entity-scoped permission checks +├── lib/ +│ ├── api-client.ts # Centralized HTTP client with JWT + 401 interception +│ └── query-client.ts # TanStack Query configuration +├── pages/ +│ ├── admin/ # Admin & LMS management (courses, batches, taxonomy, resources) +│ ├── student/ # Student portal (dashboard, courses, adaptive learning flow) +│ └── teacher/ # Teacher portal (courses, assignments, attendance, grading) +├── services/ # 21 API service modules mapped to Odoo endpoints +└── types/ # 14 TypeScript type definition files +``` + +## Key Features + +- **7 User Roles** — Student, Teacher, Admin, Corporate, Master Corporate, Agent, Developer +- **Adaptive Learning Engine** — Diagnostic assessment, proficiency profiling, AI-generated learning plans, topic-level content delivery +- **LMS Integration** — Course management, batches, timetables, attendance, grades (OpenEduCat via API) +- **Exam Portal** — IELTS-style exams with AI grading, writing evaluation, speaking assessment +- **14 AI Components** — Study coach, writing helper, grading assistant, risk detection, insights panel, batch optimizer, report narratives +- **Entity-Scoped Permissions** — Fine-grained access control with 77+ permission types +- **Multi-Subject Support** — IELTS (English), Mathematics, IT — all using a universal subject taxonomy + +## Getting Started + +### Prerequisites + +- Node.js 18+ (or Bun) +- Odoo 19 backend running (see `docs/ODOO_BACKEND_SRS_v3.md`) + +### Setup + +```bash +# Clone the repository +git clone https://git.albousalh.com/devops/encoach_frontend_new_v1.git +cd encoach_frontend_new_v1 + +# Install dependencies +npm install + +# Configure environment +cp .env.example .env.development + +# Start development server +npm run dev +``` + +The app runs on `http://localhost:8080` by default. + +### Environment Variables + +| Variable | Description | Default | +|---|---|---| +| `VITE_API_BASE_URL` | Odoo backend API base URL | `http://localhost:8069/api` | +| `VITE_APP_NAME` | Application display name | `EnCoach` | + +### Scripts + +| Command | Description | +|---|---| +| `npm run dev` | Start dev server with HMR | +| `npm run build` | Production build | +| `npm run build:dev` | Development build | +| `npm run preview` | Preview production build | +| `npm run lint` | Run ESLint | +| `npm run test` | Run tests | +| `npm run test:watch` | Run tests in watch mode | + +## Documentation + +All project documentation is in the `docs/` folder: + +| Document | Purpose | +|---|---| +| `ENCOACH_UNIFIED_SRS.md` | Master Software Requirements Specification | +| `ODOO_BACKEND_SRS_v3.md` | Backend developer handoff (Odoo modules + API contract) | +| `ENCOACH_PRODUCT_DESCRIPTION.md` | Non-technical product overview | +| `UTAS_MASTER_PLAN.md` | Project timeline and milestones | +| `MATH_IT_ADAPTIVE_LEARNING_SRS.md` | Adaptive learning engine specification | + +## Backend API + +The frontend expects an Odoo 19 backend exposing REST endpoints under `/api/`. All service modules in `src/services/` map directly to the API contract defined in `docs/ODOO_BACKEND_SRS_v3.md`. + +Authentication uses JWT tokens stored in `localStorage`, with automatic 401 interception and redirect to login. + +## License + +Proprietary — EnCoach Platform. All rights reserved. \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..9562fc50 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1222 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "vite_react_shadcn_ts", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76", + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19", + "vitest": "^3.2.4", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@adobe/css-tools/-/css-tools-4.4.4.tgz", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.0", "", { "os": "android", "cpu": "arm64" }, "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.0", "", { "os": "android", "cpu": "x64" }, "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.0", "", { "os": "none", "cpu": "x64" }, "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="], + + "@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], + + "@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.2", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.2", "", { "dependencies": { "@floating-ui/core": "^1.7.2", "@floating-ui/utils": "^0.2.10" } }, "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.4", "", { "dependencies": { "@floating-ui/dom": "^1.7.2" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.14", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="], + + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.9", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw=="], + + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.9", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-toggle": "1.1.9", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@remix-run/router": ["@remix-run/router@1.23.0", "", {}, "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.24.0", "", { "os": "android", "cpu": "arm" }, "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.24.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.24.0", "", { "os": "linux", "cpu": "arm" }, "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.24.0", "", { "os": "linux", "cpu": "arm" }, "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.24.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.24.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.24.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw=="], + + "@swc/core": ["@swc/core@1.13.2", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.2", "@swc/core-darwin-x64": "1.13.2", "@swc/core-linux-arm-gnueabihf": "1.13.2", "@swc/core-linux-arm64-gnu": "1.13.2", "@swc/core-linux-arm64-musl": "1.13.2", "@swc/core-linux-x64-gnu": "1.13.2", "@swc/core-linux-x64-musl": "1.13.2", "@swc/core-win32-arm64-msvc": "1.13.2", "@swc/core-win32-ia32-msvc": "1.13.2", "@swc/core-win32-x64-msvc": "1.13.2" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.2", "", { "os": "linux", "cpu": "arm" }, "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.2", "", { "os": "linux", "cpu": "x64" }, "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.2", "", { "os": "linux", "cpu": "x64" }, "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/types": ["@swc/types@0.1.23", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.83.0", "", {}, "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.83.0", "", { "dependencies": { "@tanstack/query-core": "5.83.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@testing-library/dom/-/dom-10.4.1.tgz", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/react": ["@testing-library/react@16.3.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@testing-library/react/-/react-16.3.2.tgz", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + + "@tootallnate/once": ["@tootallnate/once@2.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@tootallnate/once/-/once-2.0.0.tgz", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@types/aria-query/-/aria-query-5.0.4.tgz", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/chai": ["@types/chai@5.2.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@types/chai/-/chai-5.2.3.tgz", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.6", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA=="], + + "@types/d3-time": ["@types/d3-time@3.0.3", "", {}, "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@types/deep-eql/-/deep-eql-4.0.2.tgz", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.16.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ=="], + + "@types/prop-types": ["@types/prop-types@15.7.13", "", {}, "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="], + + "@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="], + + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.11.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.27", "@swc/core": "^1.12.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/expect/-/expect-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/mocker/-/mocker-3.2.4.tgz", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/runner/-/runner-3.2.4.tgz", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/snapshot/-/snapshot-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/spy/-/spy-3.2.4.tgz", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@vitest/utils/-/utils-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "abab": ["abab@2.0.6", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/abab/-/abab-2.0.6.tgz", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], + + "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-globals": ["acorn-globals@7.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/acorn-globals/-/acorn-globals-7.0.1.tgz", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-walk": ["acorn-walk@8.3.5", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/acorn-walk/-/acorn-walk-8.3.5.tgz", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "agent-base": ["agent-base@6.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/agent-base/-/agent-base-6.0.2.tgz", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], + + "aria-query": ["aria-query@5.3.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/aria-query/-/aria-query-5.3.2.tgz", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/assertion-error/-/assertion-error-2.0.1.tgz", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "asynckit": ["asynckit@0.4.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": "cli.js" }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="], + + "cac": ["cac@6.7.14", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], + + "chai": ["chai@5.3.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/chai/-/chai-5.3.3.tgz", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "check-error": ["check-error@2.1.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/check-error/-/check-error-2.1.3.tgz", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css.escape": ["css.escape@1.5.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/css.escape/-/css.escape-1.5.1.tgz", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssom": ["cssom@0.5.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/cssom/-/cssom-0.5.0.tgz", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], + + "cssstyle": ["cssstyle@2.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/cssstyle/-/cssstyle-2.3.0.tgz", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "data-urls": ["data-urls@3.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/data-urls/-/data-urls-3.0.2.tgz", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], + + "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + + "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "decimal.js": ["decimal.js@10.6.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "deep-eql": ["deep-eql@5.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/deep-eql/-/deep-eql-5.0.2.tgz", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dequal": ["dequal@2.0.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "domexception": ["domexception@4.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/domexception/-/domexception-4.0.0.tgz", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.192", "", {}, "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg=="], + + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], + + "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="], + + "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "entities": ["entities@6.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-define-property": ["es-define-property@1.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/es-module-lexer/-/es-module-lexer-1.7.0.tgz", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": "bin/esbuild" }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "escodegen": ["escodegen@2.1.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/escodegen/-/escodegen-2.1.0.tgz", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", "esgenerate": "bin/esgenerate.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.20", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esprima": ["esprima@4.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/esprima/-/esprima-4.0.1.tgz", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "expect-type": ["expect-type@1.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/expect-type/-/expect-type-1.3.0.tgz", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="], + + "fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], + + "fdir": ["fdir@6.5.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], + + "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], + + "form-data": ["form-data@4.0.5", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/form-data/-/form-data-4.0.5.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "gopd": ["gopd@1.2.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], + + "http-proxy-agent": ["http-proxy-agent@5.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "https-proxy-agent": ["https-proxy-agent@5.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "iconv-lite": ["iconv-lite@0.6.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.15.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@1.21.6", "", { "bin": "bin/jiti.js" }, "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsdom": ["jsdom@20.0.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/jsdom/-/jsdom-20.0.3.tgz", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "loupe": ["loupe@3.2.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/loupe/-/loupe-3.2.1.tgz", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "lovable-tagger": ["lovable-tagger@1.1.13", "", { "dependencies": { "esbuild": "^0.25.0", "tailwindcss": "^3.4.17" }, "peerDependencies": { "vite": ">=5.0.0 <8.0.0" } }, "sha512-RBEYDxao7Xf8ya29L0cd+ocE7Gs80xPOIOwwck65Hoie8YDKViuXi3UYV14DoNWIvaJ7WVPf7SG3cc844nFqGA=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "lucide-react": ["lucide-react@0.462.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g=="], + + "lz-string": ["lz-string@1.5.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/lz-string/-/lz-string-1.5.0.tgz", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "min-indent": ["min-indent@1.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/min-indent/-/min-indent-1.0.1.tgz", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "nwsapi": ["nwsapi@2.2.23", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/nwsapi/-/nwsapi-2.2.23.tgz", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse5": ["parse5@7.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "pathe": ["pathe@2.0.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/pathval/-/pathval-2.0.1.tgz", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], + + "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "pretty-format": ["pretty-format@27.5.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "psl": ["psl@1.15.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/psl/-/psl-1.15.0.tgz", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "querystringify": ["querystringify@2.2.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/querystringify/-/querystringify-2.2.0.tgz", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-hook-form": ["react-hook-form@7.61.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-resizable-panels": ["react-resizable-panels@2.1.9", "", { "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ=="], + + "react-router": ["react-router@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ=="], + + "react-router-dom": ["react-router-dom@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw=="], + + "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], + + "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + + "redent": ["redent@3.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/redent/-/redent-3.0.0.tgz", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "requires-port": ["requires-port@1.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/requires-port/-/requires-port-1.0.0.tgz", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + + "rollup": ["rollup@4.24.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.24.0", "@rollup/rollup-android-arm64": "4.24.0", "@rollup/rollup-darwin-arm64": "4.24.0", "@rollup/rollup-darwin-x64": "4.24.0", "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", "@rollup/rollup-linux-arm-musleabihf": "4.24.0", "@rollup/rollup-linux-arm64-gnu": "4.24.0", "@rollup/rollup-linux-arm64-musl": "4.24.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", "@rollup/rollup-linux-riscv64-gnu": "4.24.0", "@rollup/rollup-linux-s390x-gnu": "4.24.0", "@rollup/rollup-linux-x64-gnu": "4.24.0", "@rollup/rollup-linux-x64-musl": "4.24.0", "@rollup/rollup-win32-arm64-msvc": "4.24.0", "@rollup/rollup-win32-ia32-msvc": "4.24.0", "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/saxes/-/saxes-6.0.0.tgz", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], + + "source-map": ["source-map@0.6.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/std-env/-/std-env-3.10.0.tgz", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-indent": ["strip-indent@3.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/strip-indent/-/strip-indent-3.0.0.tgz", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strip-literal": ["strip-literal@3.1.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/strip-literal/-/strip-literal-3.1.0.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "symbol-tree": ["symbol-tree@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/symbol-tree/-/symbol-tree-3.2.4.tgz", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], + + "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], + + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinybench": ["tinybench@2.9.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tinyexec/-/tinyexec-0.3.2.tgz", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinypool": ["tinypool@1.1.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tinypool/-/tinypool-1.1.1.tgz", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tinyrainbow/-/tinyrainbow-2.0.0.tgz", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tinyspy/-/tinyspy-4.0.4.tgz", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tough-cookie": ["tough-cookie@4.1.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tough-cookie/-/tough-cookie-4.1.4.tgz", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], + + "tr46": ["tr46@3.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/tr46/-/tr46-3.0.0.tgz", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.0", "", {}, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "typescript-eslint": ["typescript-eslint@8.38.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/parser": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "universalify": ["universalify@0.2.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/universalify/-/universalify-0.2.0.tgz", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url-parse": ["url-parse@1.5.10", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/url-parse/-/url-parse-1.5.10.tgz", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vaul": ["vaul@0.9.9", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ=="], + + "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + + "vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="], + + "vite-node": ["vite-node@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/vite-node/-/vite-node-3.2.4.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/vitest/-/vitest-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@4.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", { "dependencies": { "xml-name-validator": "^4.0.0" } }, "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/webidl-conversions/-/webidl-conversions-7.0.0.tgz", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@2.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "whatwg-url": ["whatwg-url@11.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/whatwg-url/-/whatwg-url-11.0.0.tgz", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/why-is-node-running/-/why-is-node-running-2.3.0.tgz", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.20.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/ws/-/ws-8.20.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "xml-name-validator": ["xml-name-validator@4.0.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/xml-name-validator/-/xml-name-validator-4.0.0.tgz", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], + + "xmlchars": ["xmlchars@2.2.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yaml": ["yaml@2.6.0", "", { "bin": "bin.mjs" }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/aria-query/-/aria-query-5.3.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "agent-base/debug": ["debug@4.4.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "cssstyle/cssom": ["cssom@0.3.8", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/cssom/-/cssom-0.3.8.tgz", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "http-proxy-agent/debug": ["debug@4.4.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "https-proxy-agent/debug": ["debug@4.4.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "pretty-format/react-is": ["react-is@17.0.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "strip-literal/js-tokens": ["js-tokens@9.0.1", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "vite-node/debug": ["debug@4.4.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "vitest/debug": ["debug@4.4.3", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..5d9d33a3bf4f45fa099e2aa3014149e034981149 GIT binary patch literal 245395 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 aG(|+$-_n1#f877;&wmep(Next.js 13)"] + CMS["encoachcms
(Strapi v4)"] + end + + subgraph platformCore ["Platform Core"] + UI["ielts-ui
(Next.js 14 + API Routes)"] + BE["ielts-be
(FastAPI / Python)"] + end + + subgraph dataStores ["Data Stores"] + MongoDB["MongoDB"] + MySQL["MySQL
(CMS)"] + FirebaseAuth["Firebase Auth"] + FirebaseStorage["Firebase Storage"] + GCS["Google Cloud Storage
(CMS uploads)"] + end + + subgraph externalServices ["External Services"] + OpenAI["OpenAI GPT-4o"] + Whisper["OpenAI Whisper
(local)"] + Polly["AWS Polly
(TTS)"] + ELAI["ELAI
(AI Video)"] + GPTZero["GPTZero
(AI Detection)"] + Stripe["Stripe"] + PayPal["PayPal"] + Paymob["Paymob"] + end + + LP -->|"REST + Bearer token"| CMS + LP -->|"GET /api/packages, POST /api/tickets"| UI + LP -->|"Links to platform.encoach.com"| UI + + CMS --> MySQL + CMS --> GCS + + UI --> MongoDB + UI --> FirebaseAuth + UI -->|"Proxy: Bearer JWT"| BE + + BE --> MongoDB + BE --> FirebaseAuth + BE --> FirebaseStorage + BE --> OpenAI + BE --> Whisper + BE --> Polly + BE --> ELAI + BE --> GPTZero + + UI --> Stripe + UI --> PayPal + UI --> Paymob + LP --> Stripe +``` + +--- + +## 1. ielts-ui (Main Platform) + +**Tech:** Next.js 14, React 18, TypeScript, Tailwind CSS + DaisyUI, Zustand, SWR, iron-session, Firebase Auth, MongoDB (direct) + +**Role:** The central application. Serves the browser UI and acts as a **BFF (Backend-for-Frontend)** via Next.js API routes. It directly queries MongoDB for user/entity/group data and proxies AI-related requests to `ielts-be`. + +### Key Modules + +| Module | Description | +|--------|-------------| +| **Auth** | Firebase email/password auth + iron-session cookies. SSR-protected via `withIronSessionSsr`. | +| **Dashboards** | Role-based: student, teacher, admin, corporate, mastercorporate, developer, agent. | +| **Exams** | Taking exams (Reading, Listening, Writing, Speaking, Level). Zustand store tracks session, progress, solutions. | +| **Exam Editor** | Create/edit exams with dedicated components under `src/components/ExamEditor/`. | +| **Training** | Training content consumed from `ielts-be`. | +| **Grading** | Writing and speaking grading proxied to `ielts-be` as background tasks; UI polls for results. | +| **Entities** | Multi-tenant entity management (schools, organizations). | +| **Classrooms** | Classroom and group management for teachers. | +| **Assignments** | Assign exams to students with deadlines. | +| **Stats** | Performance statistics and PDF report generation (via `@react-pdf/renderer`). | +| **Payments** | Stripe, PayPal, Paymob webhook handlers in API routes. | +| **Tickets** | Support ticket system. | +| **User Mgmt** | Bulk import, permissions, role-based access. | + +### How it Connects to ielts-be + +API routes in `src/pages/api/` proxy to `BACKEND_URL` with `Authorization: Bearer ${BACKEND_JWT}`: + +| UI API Route | Backend Endpoint | +|--------------|------------------| +| `/api/evaluate/writing` | `BACKEND_URL/grade/writing/{task}` | +| `/api/evaluate/speaking` | `BACKEND_URL/grade/speaking/{task}` | +| `/api/transcribe` | `BACKEND_URL/listening/transcribe` | +| `/api/training` | `BACKEND_URL/training/` | +| `/api/exam/upload` | `BACKEND_URL/{endpoint}` | +| `/api/exam/media/*` | `BACKEND_URL/{endpoint}/media` | +| `/api/exam/generate/*` | `BACKEND_URL/{endpoint}` | +| `/api/exam/avatars` | `BACKEND_URL/speaking/avatars` | +| `/api/exam/[module]/import` | `BACKEND_URL/{module}/import` | +| `/api/stats/[id]/[export]/pdf` | `BACKEND_URL/grade/summary` | +| `/api/batch_users` | `BACKEND_URL/user/import` | + +### How it Connects to the Landing Page + +Exposes public-ish API routes consumed by the landing page: + +- `GET /api/packages` -- subscription packages (CORS-enabled) +- `POST /api/tickets` -- contact form submissions (CORS-enabled) +- `GET /api/users/agents` -- list sales agents (CORS-enabled) + +--- + +## 2. ielts-be (AI/ML Backend) + +**Tech:** Python 3.11, FastAPI, Motor (async MongoDB), Poetry, dependency-injector, OpenAI, Whisper, AWS Polly, ELAI, FAISS + +**Role:** Handles all AI/ML-intensive operations -- exam content generation, grading, transcription, text-to-speech, AI video, and training recommendations. Runs as a standalone service on port 8000. + +### Architecture Layers + +``` +API Routes --> Controllers --> Services --> Repositories + | + Third-Party Services + (OpenAI, Whisper, Polly, ELAI, GPTZero) +``` + +Wired together via `dependency-injector`. + +### Key Capabilities + +| Capability | Implementation | +|------------|----------------| +| **Writing Grading** | GPT-4o evaluates against IELTS rubric (Task Achievement, Coherence, Lexical Resource, Grammar). GPTZero checks for AI-generated text. Runs as background task; results stored in `evaluation` collection. | +| **Speaking Grading** | Whisper transcribes audio, then GPT grades (Fluency, Lexical, Grammar, Pronunciation). Parts 1/2/3 supported. | +| **Exam Generation** | GPT generates reading passages, listening dialogs, writing prompts, speaking tasks, and level-test exercises. | +| **Audio (TTS)** | AWS Polly synthesizes MP3 for listening sections. | +| **Video** | ELAI generates AI avatar videos for speaking prompts. | +| **Transcription** | Whisper (local model) for speech-to-text. | +| **Training** | FAISS + sentence-transformers for semantic search over tips; GPT for personalized recommendations. | +| **Short Answer Grading** | GPT-4o for reading/listening fill-in-the-blank answers. | + +### Authentication + +All endpoints (except `/api/healthcheck`) require a Bearer JWT token validated with `JWT_SECRET_KEY` (HS256). The `ielts-ui` sends this as `BACKEND_JWT`. + +--- + +## 3. encoachcms (Content Management) + +**Tech:** Strapi v4.21, Node.js 18, MySQL (prod) / SQLite (dev), Google Cloud Storage for uploads + +**Role:** Headless CMS for managing the marketing website content. Editors create/update bilingual (EN/AR) content via the Strapi admin panel at `/admin`. + +### Content Types (all Single Types) + +`home`, `about`, `services`, `contact`, `footer`, `nav-bar`, `history`, `price`, `terms-and-conditions`, `privacy-policy`, `country-managers-contact` + +### How it Connects + +- Exposes REST API at `STRAPI_URL/api/{content-type}/?populate=deep&locale={en|ar}` +- Authenticated via API token (`STRAPI_TOKEN`) +- **Only consumed by `encoach-landing-page`** -- neither `ielts-ui` nor `ielts-be` use it + +### Plugins + +- `strapi-plugin-populate-deep` -- deep relation population +- `strapi-plugin-import-export-entries` -- content import/export +- `strapi-plugin-publisher` -- scheduled publishing +- `@strapi/plugin-i18n` -- internationalization +- `@strapi/plugin-documentation` -- OpenAPI docs + +--- + +## 4. encoach-landing-page (Marketing Website) + +**Tech:** Next.js 13 (App Router), TypeScript, Tailwind CSS + DaisyUI, Stripe + +**Role:** Public marketing website at `encoach.com`. Bilingual (EN at `/`, AR at `/ar/`). Fetches content from the CMS and links users into the main platform. + +### Pages + +`/` (Home), `/about`, `/services`, `/price`, `/history`, `/contact`, `/terms`, `/privacy-policy`, `/contacts/[country]`, `/success` -- all mirrored under `/ar/` for Arabic. + +### How it Connects + +| Target | Connection | +|--------|------------| +| **encoachcms** | `getData()` fetches `STRAPI_URL/api/{page}/?populate=deep&locale={locale}` with Bearer token | +| **ielts-ui** | `GET /api/packages` for pricing, `POST /api/tickets` for contact form, `GET /api/users/agents` for sales agents | +| **Stripe** | Checkout flow; `/api/success` route validates session and POSTs to `WEBHOOK_URL` | +| **Platform links** | Buttons link to `platform.encoach.com/register`, `platform.encoach.com/official-exam` | + +--- + +## Data Flow Diagrams + +### Authentication Flow + +```mermaid +sequenceDiagram + participant Browser + participant UI as ielts-ui + participant Firebase as Firebase Auth + participant Mongo as MongoDB + + Browser->>UI: POST /api/login (email, password) + UI->>Firebase: signInWithEmailAndPassword + Firebase-->>UI: Firebase UID + token + UI->>Mongo: Find user by Firebase UID + Mongo-->>UI: User document + UI->>UI: Save to iron-session cookie + UI-->>Browser: Set-Cookie (eCrop/ielts) + Browser->>UI: Subsequent requests with cookie + UI->>UI: withIronSessionSsr validates cookie +``` + +### Exam Grading Flow (Writing) + +```mermaid +sequenceDiagram + participant Student + participant UI as ielts-ui API Route + participant BE as ielts-be + participant GPT as OpenAI GPT-4o + participant GPTZero as GPTZero + participant Mongo as MongoDB + + Student->>UI: Submit writing answer + UI->>BE: POST /api/exam/grade/writing/{task} + BE->>BE: Start background task + BE-->>UI: 200 OK (accepted) + UI-->>Student: "Grading in progress" + + par Parallel Grading + BE->>GPT: Evaluate against IELTS rubric + GPT-->>BE: Scores + feedback + and AI Detection + BE->>GPTZero: Check for AI content + GPTZero-->>BE: AI probability + end + + BE->>Mongo: Save evaluation result + Student->>UI: Poll for result + UI->>Mongo: Query evaluation + Mongo-->>UI: Grading result + UI-->>Student: Display scores + feedback +``` + +### Landing Page Content Flow + +```mermaid +sequenceDiagram + participant Visitor + participant LP as encoach-landing-page + participant CMS as encoachcms (Strapi) + participant Platform as ielts-ui + + Visitor->>LP: Visit encoach.com + LP->>CMS: GET /api/home/?populate=deep&locale=en + CMS-->>LP: Home content (JSON) + LP-->>Visitor: Rendered page + + Visitor->>LP: View pricing + LP->>CMS: GET /api/price/?populate=deep&locale=en + LP->>Platform: GET /api/packages + Platform-->>LP: Package list + LP-->>Visitor: Pricing page with packages + + Visitor->>LP: Submit contact form + LP->>Platform: POST /api/tickets + Platform-->>LP: Ticket created +``` + +--- + +## Shared Infrastructure + +| Resource | Used By | Details | +|----------|---------|---------| +| **MongoDB** | ielts-ui, ielts-be | Same database (`MONGODB_URI` / `MONGODB_DB`). UI reads users, groups, stats directly. BE reads/writes evaluations, training. | +| **Firebase Auth** | ielts-ui, ielts-be | Shared Firebase project. UI handles login/signup; BE imports users. | +| **Firebase Storage** | ielts-be | Stores exam media (audio, images). | +| **Google Cloud Storage** | encoachcms | CMS file uploads (images, media). | + +--- + +## Deployment Topology + +| Service | Port | URL (Production) | +|---------|------|-------------------| +| ielts-ui | 3000 | `platform.encoach.com` | +| ielts-be | 8000 | Internal (proxied by ielts-ui) | +| encoachcms | 1337 | Internal (consumed by landing page) | +| encoach-landing-page | 3000 | `encoach.com` | + +Both Next.js apps use `output: "standalone"` and have Dockerfiles. The BE is not directly exposed to the internet -- it sits behind ielts-ui's API route proxy layer. + +--- + +## User Roles + +| Role | Access | +|------|--------| +| **student** | Take exams, view stats, training, assignments | +| **teacher** | Manage classrooms, assignments, view student performance | +| **admin** | Full platform management, user management, entities | +| **corporate** | Corporate dashboard, bulk operations | +| **mastercorporate** | Multi-entity corporate management | +| **developer** | Developer dashboard, system tools | +| **agent** | Sales agent, visible on landing page | + +--- + +## Key Environment Variables (Cross-Service) + +| Variable | Service | Purpose | +|----------|---------|---------| +| `MONGODB_URI` / `MONGODB_DB` | ielts-ui, ielts-be | Shared MongoDB | +| `BACKEND_URL` | ielts-ui | URL of ielts-be | +| `BACKEND_JWT` / `JWT_SECRET_KEY` | ielts-ui / ielts-be | Shared JWT secret for service-to-service auth | +| `FIREBASE_*` | ielts-ui, ielts-be | Shared Firebase project credentials | +| `STRAPI_URL` / `STRAPI_TOKEN` | encoach-landing-page | CMS connection | +| `STRIPE_KEY` / `STRIPE_SECRET` | ielts-ui, encoach-landing-page | Payment processing | +| `OPENAI_API_KEY` | ielts-be | AI grading and generation | +| `AWS_ACCESS_KEY_ID/SECRET` | ielts-be | AWS Polly TTS | diff --git a/docs/DEVELOPER_FEEDBACK.md b/docs/DEVELOPER_FEEDBACK.md new file mode 100644 index 00000000..010aeabf --- /dev/null +++ b/docs/DEVELOPER_FEEDBACK.md @@ -0,0 +1,249 @@ +# EnCoach Odoo 19 -- Developer Feedback + +**Date:** March 11, 2026 +**Status:** ALL ITEMS RESOLVED (March 13, 2026) +**Reference:** ODOO_MIGRATION_SRS_v2.md + +--- + +## Overall Assessment + +The delivery covers approximately **85% of the SRS v2 requirements**. All 15 custom modules are present, the core AI/ML services are well-implemented, data models match the specification, and 67-72 API endpoints are wired up with proper JWT authentication and security groups. The code quality is solid -- clean structure, proper error handling, and good separation of concerns. + +The items below still need to be implemented to reach full SRS compliance. + +--- + +## 1. Payment Integration (Critical -- Not Implemented) + +**SRS Reference:** Section 7 + +All three payment services are currently stubs that return `"not yet implemented"`. The SRS requires full integration with: + +### 1.1 Stripe + +**Files:** `encoach_subscription/services/stripe_service.py` + +Required functionality: +- Install and use the `stripe` Python SDK +- `create_checkout_session()` -- Create a Stripe Checkout Session using `stripe.checkout.Session.create()` with: + - `line_items` from the selected `encoach.package` + - `success_url` and `cancel_url` from the request + - `client_reference_id` = user ID + - `customer_email` = user email + - Apply discount if `discount_code` is provided (validate against `encoach.discount`) +- Return `{ "sessionId": "cs_...", "url": "https://checkout.stripe.com/..." }` +- **Webhook endpoint** (`POST /api/stripe/webhook`): + - Verify signature using `stripe.Webhook.construct_event()` with `STRIPE_WEBHOOK_SECRET` + - Handle `checkout.session.completed` event: + - Find user by `client_reference_id` + - Extend `subscription_expiration` by the package's `duration_days` + - Create `encoach.subscription.payment` record + - For corporate users, propagate subscription to entity members + +### 1.2 PayPal + +**Files:** `encoach_subscription/services/paypal_service.py` + +Required functionality: +- Use PayPal REST API (`httpx` or `requests`) +- Obtain access token from `PAYPAL_ACCESS_TOKEN_URL` using `client_id` + `client_secret` +- `create_order()` -- Create PayPal order via `POST /v2/checkout/orders`: + - `intent: "CAPTURE"` + - `purchase_units` with amount from package (minus discount) + - Return `{ "orderId": "...", "approvalUrl": "https://paypal.com/..." }` +- `capture_order()` -- Capture approved order via `POST /v2/checkout/orders/{id}/capture`: + - On success, extend subscription and create payment record + - Return `{ "ok": true }` + +### 1.3 Paymob + +**Files:** `encoach_subscription/services/paymob_service.py` + +Required functionality: +- Use Paymob API (`httpx` or `requests`) +- `create_intention()` -- Create payment intention: + - Authenticate with `PAYMOB_API_KEY` + - Create order, then payment key + - Return `{ "intentionId": "...", "clientSecret": "..." }` +- `verify_transaction()` -- Verify webhook callback: + - Validate HMAC using `PAYMOB_SECRET` + - On success, extend subscription and create payment record +- **Webhook endpoint** (`POST /api/paymob/webhook`): + - Verify transaction authenticity + - Update subscription on success + +### 1.4 Subscription Extension Logic + +After any successful payment (all providers), the system must: +1. Calculate new expiration: `max(current_expiration, now()) + duration_days` +2. Update `user.subscription_expiration` +3. Create `encoach.subscription.payment` record with provider, amount, transaction ID +4. For corporate users: propagate the new expiration to all entity members + +--- + +## 2. Missing API Endpoints + +**SRS Reference:** Section 4 + +The following endpoints are specified in the SRS but not present in the delivery: + +### 2.1 Exam Variants (add to `encoach_api/controllers/generation.py` or `exams.py`) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `POST /api/exam/writing//attachment` | POST | Academic writing Task 1 with image upload (multipart form with `file` + `difficulty`). Must send image to GPT-4o vision API for prompt generation. | +| `GET /api/exam/level/` | GET | Return a pre-built level exam (no AI generation) | +| `GET /api/exam/level/utas` | GET | Return a UTAS-format level exam | +| `POST /api/exam/level/import/` | POST | Import level exam from uploaded file | +| `POST /api/exam/level/custom/` | POST | Generate custom level exam from JSON specification | +| `POST /api/exam/reading/import` | POST | Import reading exam from Word/Excel file | +| `POST /api/exam/listening/import` | POST | Import listening exam from file | + +### 2.2 Entity/Role/Permission Management (new controller files needed) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `GET /api/entities` | GET | List entities (with pagination) | +| `POST /api/entities` | POST | Create entity | +| `PATCH /api/entities/` | PATCH | Update entity | +| `DELETE /api/entities/` | DELETE | Delete entity | +| `GET /api/roles` | GET | List roles (filter by entityID) | +| `POST /api/roles` | POST | Create role | +| `PATCH /api/roles/` | PATCH | Update role | +| `DELETE /api/roles/` | DELETE | Delete role | +| `GET /api/permissions` | GET | List permissions | + +### 2.3 Other Missing Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `GET /api/discounts` | GET | List discount codes | +| `POST /api/discounts` | POST | Create discount code | +| `PATCH /api/discounts/` | PATCH | Update discount code | +| `DELETE /api/discounts/` | DELETE | Delete discount code | +| `GET /api/approval-workflows` | GET | List approval workflows | +| `POST /api/approval-workflows` | POST | Create approval workflow | +| `PATCH /api/approval-workflows/` | PATCH | Update approval workflow | +| `DELETE /api/approval-workflows/` | DELETE | Delete approval workflow | + +--- + +## 3. Evaluation Status Endpoint Path + +**SRS Reference:** Section 4.2 + +**Current:** `GET /api/evaluate/status?sessionId=...&exerciseId=...` (query params) +**SRS specifies:** `GET /api/evaluate//` (path params) + +The frontend SRS document (`ODOO_MIGRATION_FRONTEND_SRS.md`) references the path-param version. Please either: +- (A) Change the endpoint to use path params to match the SRS, OR +- (B) Confirm that query params are intentional so we can update the frontend SRS + +--- + +## 4. Record-Level Security Rules (`ir.rule`) + +**SRS Reference:** Section 5 + +The delivery has model-level ACL rules (`ir.model.access.csv`) for each module, which is good. However, record-level security rules (`ir.rule`) are also needed to ensure: + +- **Students** can only read/write their own records (sessions, stats, evaluations, training, tickets) +- **Teachers** can see records for students in their groups/assignments +- **Corporate** users can see records within their entity +- **Admin/Developer** users have unrestricted access + +Example rules needed (add to each module's `security/` directory): + +```xml + + + Students see own stats + + + [('user_id', '=', user.id)] + +``` + +Models that need record rules: +- `encoach.session` -- students see own sessions only +- `encoach.stat` -- students see own stats only +- `encoach.evaluation` -- students see own evaluations only +- `encoach.training` -- students see own training only +- `encoach.ticket` -- students see own tickets, admins see all +- `encoach.assignment` -- students see assignments they're assigned to +- `encoach.exam` -- filtered by access field (public/private/entity) + +--- + +## 5. Whisper Scaling + +**SRS Reference:** Section 6.5 + +**Current:** Single lazy-loaded Whisper model instance in `whisper_service.py` +**SRS specifies:** 4 model instances with `ThreadPoolExecutor(max_workers=4)` for parallel transcription + +Update `encoach_ai_media/services/whisper_service.py` to: +1. Load the configurable number of model instances (from `encoach.whisper_workers` system parameter, default 4) +2. Use `concurrent.futures.ThreadPoolExecutor` to process transcription requests in parallel +3. Round-robin or queue-based assignment of requests to model instances + +This is important for production performance when multiple students submit speaking exams simultaneously. + +--- + +## 6. FAISS Index Type + +**SRS Reference:** Section 6.8 + +**Current:** `faiss.IndexFlatIP` (inner product) +**SRS specifies:** `faiss.IndexFlatL2` (L2/Euclidean distance) + +The current implementation normalizes vectors and uses inner product, which effectively gives cosine similarity. This works correctly, but it's a deviation from the SRS. If the original `ielts-be` training data was built with L2 distance, the results may differ slightly. Please verify which index type the original system used and align accordingly. + +--- + +## 7. Payment Webhook Endpoints + +**SRS Reference:** Section 7 + +In addition to the payment service implementations (Item 1 above), the following webhook controller routes need to be added to `encoach_api/controllers/subscriptions.py`: + +```python +@http.route('/api/stripe/webhook', type='http', auth='public', methods=['POST'], csrf=False, cors='*') +def stripe_webhook(self, **kwargs): + """Handle Stripe webhook events (checkout.session.completed, etc.).""" + ... + +@http.route('/api/paymob/webhook', type='http', auth='public', methods=['POST'], csrf=False, cors='*') +def paymob_webhook(self, **kwargs): + """Handle Paymob transaction callbacks.""" + ... +``` + +These must be `auth='public'` (no JWT) since they're called by external services. + +--- + +## Summary of Action Items + +| # | Item | Priority | Status | +|---|------|----------|--------| +| 1 | Payment integration (Stripe/PayPal/Paymob) | **Critical** | RESOLVED | +| 2 | Missing API endpoints (~20 endpoints) | **High** | RESOLVED | +| 3 | Evaluation status endpoint path alignment | **Medium** | RESOLVED | +| 4 | `ir.rule` record-level security rules | **High** | RESOLVED | +| 5 | Whisper multi-instance scaling | **Medium** | RESOLVED | +| 6 | FAISS index type verification | **Low** | RESOLVED | +| 7 | Payment webhook endpoints | **Critical** | RESOLVED | + +--- + +## Changes Already Applied + +The following small fixes have been applied directly to the codebase: + +1. **`encoach_ai_media/__manifest__.py`** -- Added missing `external_dependencies`: `numpy`, `openai-whisper`, `librosa`, `soundfile`, `httpx` +2. **`encoach_training/__manifest__.py`** -- Added missing `external_dependencies`: `numpy`, `faiss-cpu`, `sentence-transformers` +3. **`requirements.txt`** -- Created at project root with all Python dependencies and versions from SRS Section 6.10 diff --git a/docs/ENCOACH_ODOO19_BACKEND_SRS.md b/docs/ENCOACH_ODOO19_BACKEND_SRS.md new file mode 100644 index 00000000..f93faaad --- /dev/null +++ b/docs/ENCOACH_ODOO19_BACKEND_SRS.md @@ -0,0 +1,1959 @@ +# EnCoach Platform — Odoo 19 Backend SRS + +**Version:** 3.0 +**Date:** March 11, 2026 +**Status:** Implemented — Staging Verified +**Supersedes:** `ODOO_BACKEND_SRS_v3.md`, `ODOO_MIGRATION_SRS_v2.md`, `ODOO_MIGRATION_SRS.md` +**Frontend Reference:** `ENCOACH_UNIFIED_SRS.md` (v2.0) +**Purpose:** Complete backend specification for the EnCoach Adaptive Learning Platform on Odoo 19. All modules, models, and REST API endpoints have been implemented and deployed to staging. + +### Implementation Status + +| Artifact | Location | +|----------|----------| +| **Backend Repository** | `https://git.albousalh.com/devops/encoach_backend_new_v2.git` (branch: `main`) | +| **Frontend Repository** | `https://git.albousalh.com/devops/encoach_frontend_new_v2.git` (branch: `main`) | +| **Staging Backend (Odoo 19)** | `http://5.189.151.117:8069` | +| **Staging Frontend** | `http://5.189.151.117:3000` | +| **Custom EnCoach Modules** | 27 modules in `new_project/custom_addons/` | +| **OpenEduCat Modules** | 14 community modules in `new_project/openeducat_erp-19.0/` | +| **Total API Routes** | ~377 REST endpoints across 4 controller packages | +| **Deployment** | Docker Compose (Odoo 19 + PostgreSQL 16) | + +**Note:** The developer implemented all features in this SRS plus additional OpenEduCat Enterprise features documented in Section 34 (Beyond-SRS Features). + +--- + +## Table of Contents + +1. [Introduction](#1-introduction) +2. [Architecture Overview](#2-architecture-overview) +3. [OpenEduCat Integration Strategy](#3-openeducat-integration-strategy) +4. [Module Inventory](#4-module-inventory) +5. [Authentication and User Management](#5-authentication-and-user-management) +6. [Entity and Permission System](#6-entity-and-permission-system) +7. [LMS Core (OpenEduCat Bridge)](#7-lms-core-openeducat-bridge) +8. [Academic Year, Term, and Department](#8-academic-year-term-and-department) +9. [Admission and Enrollment](#9-admission-and-enrollment) +10. [Subject Registration](#10-subject-registration) +11. [Exam Engine (EnCoach AI)](#11-exam-engine-encoach-ai) +12. [Institutional Exams (OpenEduCat)](#12-institutional-exams-openeducat) +13. [Assignment System](#13-assignment-system) +14. [Course Assignment Submissions (OpenEduCat)](#14-course-assignment-submissions-openeducat) +15. [Classroom and Group Management](#15-classroom-and-group-management) +16. [Subject Taxonomy Engine](#16-subject-taxonomy-engine) +17. [Adaptive Learning Engine](#17-adaptive-learning-engine) +18. [Learning Resources](#18-learning-resources) +19. [AI Services](#19-ai-services) +20. [Training Content](#20-training-content) +21. [Analytics and Statistics](#21-analytics-and-statistics) +22. [Subscription and Payments](#22-subscription-and-payments) +23. [Support Tickets](#23-support-tickets) +24. [File Storage](#24-file-storage) +25. [Approval Workflows (Enhanced)](#25-approval-workflows-enhanced) +26. [Courseware and Content Delivery](#26-courseware-and-content-delivery) +27. [Communication System](#27-communication-system) +28. [Notification Engine](#28-notification-engine) +29. [FAQ System](#29-faq-system) +30. [Plagiarism Detection](#30-plagiarism-detection) +31. [Official Exam Access](#31-official-exam-access) +32. [Non-Functional Requirements](#32-non-functional-requirements) +33. [Implementation Priority](#33-implementation-priority) +34. [Beyond-SRS Features (Implemented)](#34-beyond-srs-features-implemented) + +--- + +## 1. Introduction + +### 1.1 Scope + +This document specifies the complete Odoo 19 backend for the EnCoach Adaptive Learning Platform. The backend serves a React SPA frontend via REST API. All endpoints use JSON request/response with JWT authentication (except explicitly public endpoints). + +### 1.2 Frontend Contract + +The React frontend has 29 API service modules that define the exact endpoints the backend must implement. This SRS documents every endpoint, its expected request/response shape, and the underlying Odoo models. + +### 1.3 Key Principles + +- **OpenEduCat First:** All traditional LMS features (courses, batches, students, faculty, timetable, attendance, exams, assignments, admissions) use OpenEduCat community modules ported to Odoo 19. +- **EnCoach Extensions:** Custom modules extend OpenEduCat models with fields the platform needs (e.g., `description`, `image`, `max_capacity` on `op.course`). +- **REST API Layer:** All data is exposed via REST controllers in `encoach_api` and `encoach_lms_api` modules. The frontend never uses Odoo's web client or XML-RPC. +- **AI Services:** AI functionality (GPT, Whisper, Polly, ELAI, GPTZero, FAISS) is encapsulated in dedicated `encoach_ai_*` modules with API endpoints. + +--- + +## 2. Architecture Overview + +``` +React SPA (Vite + TypeScript) + │ + ▼ REST API (JSON + JWT) +┌─────────────────────────────────┐ +│ Odoo 19 Server │ +│ │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ encoach_ │ │ OpenEduCat │ │ +│ │ modules │ │ (ported v19)│ │ +│ └──────────┘ └──────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ PostgreSQL 16 │ +└─────────────────────────────────┘ + │ + ▼ External Services + OpenAI API │ AWS Polly │ ELAI │ GPTZero +``` + +### 2.1 Technology Stack + +| Layer | Technology | +|-------|-----------| +| Framework | Odoo 19 Community Edition | +| Language | Python 3.12+ | +| Database | PostgreSQL 16 | +| LMS Foundation | OpenEduCat (ported from v17 to v19) | +| AI/ML | OpenAI GPT-4o, Whisper (local), AWS Polly, Sentence Transformers, FAISS | +| Authentication | JWT (PyJWT) | +| File Storage | Odoo ir.attachment + optional S3 | +| Deployment | Docker, Docker Compose | + +--- + +## 3. OpenEduCat Integration Strategy + +### 3.1 Modules to Port (v17 → v19) + +| Module | Models | Porting Notes | +|--------|--------|---------------| +| `openeducat_core` | `op.course`, `op.batch`, `op.student`, `op.faculty`, `op.subject`, `op.department`, `op.category`, `op.academic.year`, `op.academic.term`, `op.student.course`, `op.subject.registration` | Core dependency for all other modules | +| `openeducat_timetable` | `op.session`, `op.timing` | Depends on `openeducat_core` | +| `openeducat_attendance` | `op.attendance.register`, `op.attendance.sheet`, `op.attendance.line`, `op.attendance.type` | Depends on `openeducat_core`, `openeducat_timetable` | +| `openeducat_classroom` | `op.classroom` | Depends on `openeducat_facility` | +| `openeducat_facility` | `op.facility`, `op.facility.line` | Dependency of classroom | +| `openeducat_exam` | `op.exam.session`, `op.exam`, `op.exam.type`, `op.exam.attendees`, `op.exam.room`, `op.marksheet.register`, `op.marksheet.line`, `op.result.line`, `op.result.template`, `op.grade.configuration` | Depends on `openeducat_core` | +| `openeducat_assignment` | `op.assignment`, `op.assignment.sub.line`, `grading.assignment`, `grading.assignment.type` | Depends on `openeducat_core` | +| `openeducat_admission` | `op.admission`, `op.admission.register` | Depends on `openeducat_core` | + +### 3.2 Model Extensions (via `encoach_lms_api`) + +The `encoach_lms_api` module uses `_inherit` to add fields missing from OpenEduCat but required by the frontend: + +| Model | Added Fields | +|-------|-------------| +| `op.course` | `description` (Text), `image` (Binary), `max_capacity` (Integer), `status` (Selection: draft/active/archived), `department_id` (Many2one to `op.department`) | +| `op.batch` | `max_students` (Integer), `status` (Selection: draft/active/completed) | +| `op.student` | `enrollment_date` (Date), `status` (Selection: active/inactive/graduated/suspended) | +| `op.faculty` | `department_id` (Many2one to `op.department`), `specialization` (Char) | +| `op.subject` | `credits` (Float), `department_id` (Many2one to `op.department`) | + +### 3.3 Serialization + +All OpenEduCat models need `to_api_dict()` methods on the `encoach_lms_api` inheriting models that convert records to JSON-serializable dictionaries with snake_case keys. Many2one fields are serialized as `{field}_id` (integer) and `{field}_name` (string). + +--- + +## 4. Module Inventory + +| # | Module | Type | Description | +|---|--------|------|-------------| +| 1 | `openeducat_core` | OpenEduCat (port v19) | Course, batch, student, faculty, subject, department, academic year/term | +| 2 | `openeducat_timetable` | OpenEduCat (port v19) | Timetable sessions and timings | +| 3 | `openeducat_attendance` | OpenEduCat (port v19) | Attendance sheets, lines, registers | +| 4 | `openeducat_classroom` | OpenEduCat (port v19) | Physical classrooms | +| 5 | `openeducat_facility` | OpenEduCat (port v19) | Facility management | +| 6 | `openeducat_exam` | OpenEduCat (port v19) | Institutional exam sessions, marksheets, grading | +| 7 | `openeducat_assignment` | OpenEduCat (port v19) | Course assignments and submissions | +| 8 | `openeducat_admission` | OpenEduCat (port v19) | Admission registers and applications | +| 9 | `encoach_core` | Custom | `encoach.user` extensions on `res.users`, entities, roles, permissions, codes, invites | +| 10 | `encoach_api` | Custom | REST API controllers for auth, users, entities, permissions, exams, assignments, classrooms, tickets, subscriptions, stats, storage, approvals | +| 11 | `encoach_lms_api` | Custom | REST API bridge for OpenEduCat models (courses, batches, timetable, attendance, grades, academic years, departments, admissions, subject registration, institutional exams, marksheets, course assignments) | +| 12 | `encoach_ai` | Custom | OpenAI service wrapper, token counting, blacklist, system parameter management | +| 13 | `encoach_ai_media` | Custom | TTS (AWS Polly), STT (Whisper), ELAI avatar video generation | +| 14 | `encoach_ai_generation` | Custom | AI exam content generation for all subjects | +| 15 | `encoach_ai_grading` | Custom | AI grading for writing, speaking, math, IT | +| 16 | `encoach_exam` | Custom | `encoach.exam` model for AI-powered exams | +| 17 | `encoach_assignment` | Custom | `encoach.assignment` model (exam-wrapper assignments) | +| 18 | `encoach_evaluation` | Custom | Evaluation job tracking | +| 19 | `encoach_stats` | Custom | Session tracking and statistics | +| 20 | `encoach_training` | Custom | Training tips, walkthroughs, FAISS embeddings | +| 21 | `encoach_classroom` | Custom | `encoach.classroom.group` for virtual classrooms | +| 22 | `encoach_subscription` | Custom | Packages, payments, discounts, Stripe/PayPal/Paymob | +| 23 | `encoach_registration` | Custom | User registration and batch import | +| 24 | `encoach_ticket` | Custom | Support tickets | +| 25 | `encoach_taxonomy` | Custom | Subject, domain, topic, learning objective models | +| 26 | `encoach_resources` | Custom | Learning resource library | +| 27 | `encoach_adaptive` | Custom | Proficiency tracking, learning plans, progress, content cache | +| 28 | `encoach_adaptive_api` | Custom | REST controllers for adaptive learning | +| 29 | `encoach_adaptive_ai` | Custom | AI services for diagnostics, plan generation, coaching, content generation | +| 30 | `encoach_sis` | Custom | UTAS SIS integration (sync, mapping) | +| 31 | `encoach_branding` | Custom | Whitelabeling configuration | +| 32 | `encoach_courseware` | Custom | Course chapters, chapter materials, chapter progress tracking, AI workbench for content generation | +| 33 | `encoach_communication` | Custom | Discussion boards, threaded posts, announcements, direct messaging | +| 34 | `encoach_notification` | Custom | Notification engine, configurable rules, email/in-app delivery, user preferences | +| 35 | `encoach_faq` | Custom | FAQ categories and items, role-filtered, searchable | + +**Original plan: 8 OpenEduCat + 27 custom = 35 modules** + +### 4.2 Additional Modules (Implemented Beyond SRS) + +The developer added 6 more OpenEduCat community modules: + +| # | Module | Type | Description | +|---|--------|------|-------------| +| 36 | `openeducat_fees` | OpenEduCat (port v19) | Fee plans, student fees, fee terms | +| 37 | `openeducat_library` | OpenEduCat (port v19) | Media catalog, movements, library cards | +| 38 | `openeducat_activity` | OpenEduCat (port v19) | Student activities, activity types | +| 39 | `openeducat_parent` | OpenEduCat (port v19) | Parent-student relationships | +| 40 | `openeducat_erp` | OpenEduCat (port v19) | ERP connector meta-module | +| 41 | `theme_web_openeducat` | OpenEduCat (port v19) | Web theme | + +**Deployed total: 14 OpenEduCat + 27 custom = 41 modules** + +--- + +## 5. Authentication and User Management + +### 5.1 Authentication Model + +JWT-based authentication. Tokens are issued on login and validated on every API request. + +### 5.2 User Model + +Extends `res.users` with `encoach.user` mixin: + +| Field | Type | Description | +|-------|------|-------------| +| `user_type` | Selection | `student`, `teacher`, `admin`, `corporate`, `mastercorporate`, `agent`, `developer` | +| `is_verified` | Boolean | Email verification status | +| `entity_ids` | Many2many | Associated entities | +| `phone` | Char | Phone number | +| `birth_date` | Date | Date of birth | +| `gender` | Selection | `m`, `f`, `o` | +| `op_student_id` | Many2one | Link to `op.student` if user_type is student | +| `op_faculty_id` | Many2one | Link to `op.faculty` if user_type is teacher | + +### 5.3 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/login` | Public | Authenticate user, return JWT + user object | +| `POST` | `/api/logout` | JWT | Invalidate token | +| `GET` | `/api/user` | JWT | Get current authenticated user | +| `POST` | `/api/reset/sendVerification` | Public | Send password reset / verification email | +| `GET` | `/api/users/list` | JWT | Paginated user list with filters (`type`, `entity_id`, `page`, `size`) | +| `GET` | `/api/users/{id}` | JWT | Get user by ID | +| `PATCH` | `/api/users/update` | JWT | Update user (id in body) | +| `POST` | `/api/users/create` | JWT | Create user | +| `POST` | `/api/batch_users` | JWT | Batch create users from CSV/JSON | +| `GET` | `/api/users/export` | JWT | Export users as CSV blob | + +### 5.4 Login Response Shape + +```json +{ + "token": "eyJ...", + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "user_type": "admin", + "is_verified": true, + "entities": [ + { "id": 1, "name": "UTAS", "logo": "..." } + ] + } +} +``` + +--- + +## 6. Entity and Permission System + +### 6.1 Entity Model (`encoach.entity`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Entity name | +| `code` | Char | Unique code | +| `type` | Selection | `corporate`, `university`, `freelance` | +| `logo` | Binary | Entity logo | +| `active` | Boolean | Active status | +| `user_ids` | Many2many | Members | +| `role_ids` | One2many | Custom roles | + +### 6.2 Role and Permission Models + +**`encoach.role`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Role name | +| `entity_id` | Many2one | Parent entity | +| `permission_ids` | Many2many | Assigned permissions | + +**`encoach.permission`** + +77+ entity-scoped permissions (e.g., `view_students`, `edit_exam`, `view_payment_record`, `manage_entities`). + +### 6.3 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/entities` | JWT | Paginated entities | +| `GET` | `/api/entities/{id}` | JWT | Entity detail | +| `POST` | `/api/entities` | JWT | Create entity | +| `PATCH` | `/api/entities/{id}` | JWT | Update entity | +| `DELETE` | `/api/entities/{id}` | JWT | Delete entity | +| `GET` | `/api/entities/{entityId}/roles` | JWT | Roles for entity | +| `POST` | `/api/entities/{entityId}/roles` | JWT | Create role | +| `PATCH` | `/api/roles/{roleId}/permissions` | JWT | Update role permissions | +| `GET` | `/api/permissions` | JWT | List permissions (filter by `entity_id`) | + +--- + +## 7. LMS Core (OpenEduCat Bridge) + +### 7.1 Course API + +Backed by `op.course` (extended via `_inherit` in `encoach_lms_api`). + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/courses` | JWT | Paginated courses (filter: `status`, `department_id`) | +| `GET` | `/api/courses/{id}` | JWT | Course detail with subjects, batches | +| `POST` | `/api/courses` | JWT | Create course | +| `PATCH` | `/api/courses/{id}` | JWT | Update course | +| `DELETE` | `/api/courses/{id}` | JWT | Delete course | +| `POST` | `/api/courses/ai-generate` | JWT | AI-generate course outline (calls OpenAI) | + +**Course Response Shape:** + +```json +{ + "id": 1, + "name": "IELTS Preparation", + "code": "IELTS-101", + "description": "Comprehensive IELTS preparation course", + "department_id": 1, + "department_name": "English", + "max_capacity": 30, + "status": "active", + "subject_ids": [1, 2, 3], + "subject_names": ["Reading", "Writing", "Listening"], + "batch_count": 2, + "student_count": 45, + "image": "base64..." +} +``` + +### 7.2 Batch API + +Backed by `op.batch` (extended). + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/batches` | JWT | Paginated batches (filter: `course_id`, `status`) | +| `GET` | `/api/batches/{id}` | JWT | Batch detail with student list | +| `POST` | `/api/batches` | JWT | Create batch | +| `PATCH` | `/api/batches/{id}` | JWT | Update batch | +| `DELETE` | `/api/batches/{id}` | JWT | Delete batch | + +### 7.3 Timetable API + +Backed by `op.session` and `op.timing`. + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/timetable` | JWT | Sessions (filter: `course_id`, `batch_id`, `faculty_id`, `start_date`, `end_date`) | +| `POST` | `/api/timetable` | JWT | Create timetable session | + +**Session Response Shape:** + +```json +{ + "id": 1, + "name": "IELTS Reading - Week 1", + "course_id": 1, + "course_name": "IELTS Preparation", + "batch_id": 1, + "batch_name": "Batch A", + "faculty_id": 5, + "faculty_name": "Dr. Smith", + "subject_id": 1, + "subject_name": "Reading", + "classroom_id": 3, + "classroom_name": "Room 101", + "start_datetime": "2026-03-15T09:00:00", + "end_datetime": "2026-03-15T10:30:00", + "day": "Monday" +} +``` + +### 7.4 Attendance API + +Backed by `op.attendance.sheet` and `op.attendance.line`. + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/attendance` | JWT | Attendance records (filter: `course_id`, `batch_id`, `student_id`, `date_from`, `date_to`) | +| `POST` | `/api/attendance` | JWT | Record attendance (batch of student attendance lines) | + +**Attendance Record Request:** + +```json +{ + "session_id": 1, + "attendance_date": "2026-03-15", + "lines": [ + { "student_id": 1, "present": true }, + { "student_id": 2, "present": false, "remark": "Sick leave" } + ] +} +``` + +### 7.5 Grades API + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/grades` | JWT | Grade records (filter: `student_id`, `course_id`, `batch_id`) | + +--- + +## 8. Academic Year, Term, and Department + +### 8.1 Academic Year and Term + +Backed by `op.academic.year` and `op.academic.term` (OpenEduCat core). + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/academic-years` | JWT | List academic years with terms | +| `GET` | `/api/academic-years/{id}` | JWT | Year detail with terms | +| `POST` | `/api/academic-years` | JWT | Create academic year | +| `PATCH` | `/api/academic-years/{id}` | JWT | Update year | +| `DELETE` | `/api/academic-years/{id}` | JWT | Delete year | +| `POST` | `/api/academic-years/{id}/generate-terms` | JWT | Auto-generate terms from `term_structure` | +| `GET` | `/api/academic-terms` | JWT | List terms (filter: `year_id`) | +| `POST` | `/api/academic-terms` | JWT | Create term | +| `PATCH` | `/api/academic-terms/{id}` | JWT | Update term | +| `DELETE` | `/api/academic-terms/{id}` | JWT | Delete term | + +**Generate Terms Logic:** + +Given `term_structure` on the academic year, compute term dates: +- `two_sem`: 2 terms splitting the year in half +- `two_sem_qua`: 2 semesters, each with 2 quarters (4 terms total) +- `three_sem`: 3 trimesters +- `four_Quarter`: 4 quarters +- `final_year`: single term spanning the full year + +### 8.2 Department + +Backed by `op.department` (OpenEduCat core). + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/departments` | JWT | List departments | +| `POST` | `/api/departments` | JWT | Create department | +| `PATCH` | `/api/departments/{id}` | JWT | Update department | +| `DELETE` | `/api/departments/{id}` | JWT | Delete department | + +**Department Response:** + +```json +{ + "id": 1, + "name": "Department of Mathematics", + "code": "MATH", + "parent_id": null, + "parent_name": null, + "course_count": 5, + "faculty_count": 12 +} +``` + +--- + +## 9. Admission and Enrollment + +### 9.1 Admission Register + +Backed by `op.admission.register` (OpenEduCat). + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/admission-registers` | JWT | List registers | +| `POST` | `/api/admission-registers` | JWT | Create register | +| `PATCH` | `/api/admission-registers/{id}` | JWT | Update register | +| `POST` | `/api/admission-registers/{id}/confirm` | JWT | Confirm register (opens enrollment) | +| `POST` | `/api/admission-registers/{id}/close` | JWT | Close register | + +### 9.2 Admission + +Backed by `op.admission` (OpenEduCat). + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/admissions` | JWT | List admissions (filter: `state`, `register_id`, `course_id`, `page`, `size`) | +| `GET` | `/api/admissions/{id}` | JWT | Admission detail | +| `POST` | `/api/admissions` | Public* | Submit admission application | +| `POST` | `/api/admissions/{id}/submit` | JWT | Submit for review | +| `POST` | `/api/admissions/{id}/confirm` | JWT | Confirm admission | +| `POST` | `/api/admissions/{id}/admit` | JWT | Admit applicant | +| `POST` | `/api/admissions/{id}/reject` | JWT | Reject admission | + +*Note: `POST /api/admissions` is public when submitted via the `/apply` page. Backend should allow unauthenticated submission when the register allows public applications. + +### 9.3 Admit Action Logic + +When an admission is admitted (`/admit`): +1. Create `op.student` record from admission data +2. Create `res.users` record with `user_type=student` +3. Create `op.student.course` linking student to course/batch +4. Set `admission.student_id` to the new student +5. Set `admission.state` to `admission` +6. Send welcome email with login credentials + +--- + +## 10. Subject Registration + +### 10.1 Model + +Backed by `op.subject.registration` (OpenEduCat core). + +### 10.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/subject-registrations` | JWT | List registrations (filter: `student_id`, `course_id`, `state`) | +| `POST` | `/api/subject-registrations` | JWT | Create registration | +| `PATCH` | `/api/subject-registrations/{id}` | JWT | Update selected subjects | +| `POST` | `/api/subject-registrations/{id}/confirm` | JWT | Confirm registration | +| `POST` | `/api/subject-registrations/{id}/reject` | JWT | Reject registration | +| `GET` | `/api/subject-registrations/available` | JWT | Get available subjects for current student's enrolled courses | + +### 10.3 Available Subjects Logic + +For the current user (student): +1. Find all `op.student.course` records for this student +2. Get subjects linked to those courses +3. Exclude subjects already registered in confirmed registrations +4. Check unit load limits from `op.course` configuration +5. Return list with `min_unit_load`, `max_unit_load` + +--- + +## 11. Exam Engine (EnCoach AI) + +### 11.1 Model (`encoach.exam`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Exam title | +| `module` | Selection | `reading`, `writing`, `listening`, `speaking`, `math`, `it`, ... | +| `subject_id` | Many2one | Link to `encoach.subject` (taxonomy) | +| `topic_ids` | Many2many | Link to `encoach.topic` | +| `entity_id` | Many2one | Owning entity | +| `structure_id` | Many2one | Exam structure template | +| `rubric_id` | Many2one | Grading rubric | +| `exercises` | Text (JSON) | Generated exercises content | +| `is_public` | Boolean | Public access flag | +| `status` | Selection | `draft`, `published`, `archived` | + +### 11.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/exam/{module}` | JWT | List exams for module (paginated) | +| `GET` | `/api/exam/{module}/{id}` | JWT | Exam detail | +| `POST` | `/api/exam` | JWT | Create exam | +| `PATCH` | `/api/exam/{id}` | JWT | Update exam | +| `DELETE` | `/api/exam/{id}` | JWT | Delete exam | +| `PATCH` | `/api/exam/{id}/access` | JWT | Set public/private | +| `GET` | `/api/rubrics` | JWT | List rubrics (paginated) | +| `POST` | `/api/rubrics` | JWT | Create rubric | +| `GET` | `/api/rubric-groups` | JWT | List rubric groups | +| `GET` | `/api/exam-structures` | JWT | List exam structures | +| `POST` | `/api/exam-structures` | JWT | Create exam structure | +| `DELETE` | `/api/exam-structures/{id}` | JWT | Delete structure | +| `GET` | `/api/exam/avatars` | JWT | List ELAI avatars | +| `POST` | `/api/exam/{module}/generate` | JWT | Generate exercises from existing exam | +| `POST` | `/api/exam/{module}/generate/scratch` | JWT | Generate full exam from scratch | + +### 11.3 Generation Logic + +Uses OpenAI GPT-4o to generate exam content. Prompt templates vary by module: +- **Reading/Listening:** Passage + comprehension questions +- **Writing:** Task description + sample topics +- **Speaking:** Dialogue prompts + follow-up questions +- **Math:** Problem sets with numerical answers, configurable difficulty +- **IT:** Code completion, debugging, conceptual questions + +--- + +## 12. Institutional Exams (OpenEduCat) + +### 12.1 Overview + +Traditional institutional exams (midterms, finals) managed through OpenEduCat. Separate from EnCoach AI exams. + +### 12.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/inst-exam-sessions` | JWT | List exam sessions | +| `POST` | `/api/inst-exam-sessions` | JWT | Create session | +| `GET` | `/api/inst-exam-sessions/{id}` | JWT | Session detail with exams | +| `PATCH` | `/api/inst-exam-sessions/{id}` | JWT | Update session | +| `DELETE` | `/api/inst-exam-sessions/{id}` | JWT | Delete session | +| `POST` | `/api/inst-exam-sessions/{id}/schedule` | JWT | Schedule session | +| `POST` | `/api/inst-exam-sessions/{id}/done` | JWT | Mark session done | +| `POST` | `/api/inst-exams` | JWT | Add exam to session | +| `PATCH` | `/api/inst-exams/{id}` | JWT | Update exam | +| `POST` | `/api/inst-exams/{id}/attendees` | JWT | Add attendees to exam | +| `PATCH` | `/api/inst-exams/{id}/marks` | JWT | Enter marks for attendees | +| `GET` | `/api/exam-types` | JWT | List exam types | +| `POST` | `/api/exam-types` | JWT | Create exam type | +| `GET` | `/api/grade-config` | JWT | List grade configurations | +| `POST` | `/api/grade-config` | JWT | Create grade config | +| `GET` | `/api/result-templates` | JWT | List result templates | +| `POST` | `/api/result-templates` | JWT | Create result template | +| `POST` | `/api/result-templates/{id}/generate` | JWT | Generate marksheets | +| `GET` | `/api/marksheets` | JWT | List marksheets | +| `GET` | `/api/marksheets/{id}` | JWT | Marksheet detail with student results | +| `POST` | `/api/marksheets/{id}/validate` | JWT | Validate marksheet | + +### 12.3 Enter Marks Request + +```json +{ + "attendees": [ + { "student_id": 1, "marks": 85 }, + { "student_id": 2, "marks": 72 } + ] +} +``` + +### 12.4 Generate Marksheets Logic + +1. Create `op.marksheet.register` from `op.result.template` +2. For each student in the batch: + - Create `op.marksheet.line` + - For each exam in the session: create `op.result.line` with marks from `op.exam.attendees` + - Compute total marks, percentage + - If evaluation_type is `grade`: lookup `op.grade.configuration` to assign grade + - Compute pass/fail from `min_marks` on each exam +3. Compute `total_pass` and `total_failed` on register + +--- + +## 13. Assignment System (EnCoach AI) + +### 13.1 Model (`encoach.assignment`) + +Exam-wrapper assignments where an EnCoach exam is assigned to students/classrooms. + +### 13.2 Enhanced Fields (Late Submission + Plagiarism) + +Add to `encoach.assignment`: + +| Field | Type | Description | +|-------|------|-------------| +| `late_deadline` | Datetime | Final late submission cutoff | +| `penalty_type` | Selection | `none`, `percentage`, `fixed_deduction` | +| `penalty_value` | Float | Penalty amount | +| `max_submissions` | Integer | Maximum allowed submissions (0 = unlimited) | +| `allow_extensions` | Boolean | Student extension requests | +| `plagiarism_enabled` | Boolean | Enable GPTZero plagiarism check | +| `plagiarism_max_checks` | Integer | Max plagiarism checks per submission | +| `reminder_count` | Integer | Number of reminders | +| `reminder_frequency` | Selection | `once`, `daily`, `custom` | + +### 13.3 Extension Request Model (`encoach.extension.request`) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one (`res.users`) | Requesting student | +| `assignment_id` | Many2one | Target assignment | +| `reason` | Text | Justification | +| `requested_date` | Datetime | New deadline | +| `status` | Selection | `pending`, `approved`, `rejected` | +| `approved_by` | Many2one | Approving teacher | +| `response_note` | Text | Teacher response | + +### 13.4 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/assignments` | JWT | Paginated assignments | +| `GET` | `/api/assignments/{id}` | JWT | Assignment detail | +| `POST` | `/api/assignments` | JWT | Create assignment (includes late submission fields) | +| `PATCH` | `/api/assignments/{id}` | JWT | Update assignment | +| `DELETE` | `/api/assignments/{id}` | JWT | Delete assignment | +| `POST` | `/api/assignments/{id}/archive` | JWT | Archive assignment | +| `POST` | `/api/assignments/{id}/start` | JWT | Start assignment (student) | +| `POST` | `/api/assignments/{id}/request-extension` | JWT (student) | Request deadline extension | +| `GET` | `/api/assignments/{id}/extension-requests` | JWT (teacher) | List extension requests | +| `POST` | `/api/extension-requests/{id}/respond` | JWT (teacher) | Approve/reject extension | + +--- + +## 14. Course Assignment Submissions (OpenEduCat) + +### 14.1 Overview + +Traditional file-based assignments where students upload work and teachers grade it. Uses OpenEduCat models. + +### 14.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/course-assignments` | JWT | List course assignments (filter: `course_id`, `batch_id`, `state`) | +| `POST` | `/api/course-assignments` | JWT | Create course assignment | +| `GET` | `/api/course-assignments/{id}` | JWT | Assignment detail with submissions | +| `PATCH` | `/api/course-assignments/{id}` | JWT | Update assignment | +| `POST` | `/api/course-assignments/{id}/publish` | JWT | Publish assignment | +| `POST` | `/api/course-assignments/{id}/finish` | JWT | Close assignment | +| `GET` | `/api/course-assignments/{id}/submissions` | JWT | List student submissions | +| `POST` | `/api/course-assignments/{id}/submit` | JWT | Student file upload (multipart/form-data) | +| `PATCH` | `/api/submissions/{id}` | JWT | Grade submission (marks, feedback, status) | +| `GET` | `/api/assignment-types` | JWT | List assignment types | +| `POST` | `/api/assignment-types` | JWT | Create assignment type | + +### 14.3 File Upload + +Student submission accepts `multipart/form-data`: +- `file`: Binary file attachment +- `description`: Optional text description + +Files are stored as `ir.attachment` records linked to `op.assignment.sub.line`. + +### 14.4 Grading Request + +```json +{ + "marks": 85, + "state": "accept", + "note": "Well researched paper. Minor formatting issues." +} +``` + +--- + +## 15. Classroom and Group Management + +### 15.1 Model (`encoach.classroom.group`) + +Virtual classrooms for grouping students within entities. + +### 15.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/groups` | JWT | Paginated groups | +| `GET` | `/api/groups/{id}` | JWT | Group detail with members | +| `POST` | `/api/groups` | JWT | Create group | +| `DELETE` | `/api/groups/{id}` | JWT | Delete group | +| `POST` | `/api/groups/transfer` | JWT | Transfer students between groups | +| `POST` | `/api/groups/{id}/members` | JWT | Add members | +| `POST` | `/api/groups/{id}/members/remove` | JWT | Remove members | + +--- + +## 16. Subject Taxonomy Engine + +### 16.1 Models + +**`encoach.subject`** (distinct from `op.subject`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Subject name (e.g., "IELTS", "Mathematics", "Information Technology") | +| `code` | Char | Unique code | +| `description` | Text | Description | +| `icon` | Char | Icon identifier | +| `active` | Boolean | Active flag | +| `domain_ids` | One2many | Child domains | + +**`encoach.domain`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Domain name (e.g., "Algebra", "Networking") | +| `subject_id` | Many2one | Parent subject | +| `topic_ids` | One2many | Child topics | + +**`encoach.topic`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Topic name | +| `domain_id` | Many2one | Parent domain | +| `subject_id` | Many2one (related) | Subject via domain | +| `difficulty` | Selection | `beginner`, `intermediate`, `advanced` | +| `prerequisite_ids` | Many2many | Prerequisite topics | +| `learning_objective_ids` | One2many | Learning objectives | + +**`encoach.learning.objective`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Objective description | +| `topic_id` | Many2one | Parent topic | +| `bloom_level` | Selection | `remember`, `understand`, `apply`, `analyze`, `evaluate`, `create` | + +### 16.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/subjects` | JWT | List subjects | +| `GET` | `/api/subjects/{id}` | JWT | Subject detail | +| `POST` | `/api/subjects` | JWT | Create subject | +| `PATCH` | `/api/subjects/{id}` | JWT | Update subject | +| `DELETE` | `/api/subjects/{id}` | JWT | Delete subject | +| `GET` | `/api/subjects/{subjectId}/taxonomy` | JWT | Full taxonomy tree (domains → topics → objectives) | +| `POST` | `/api/subjects/{subjectId}/taxonomy/import` | JWT | Import taxonomy from file (multipart) | +| `GET` | `/api/domains` | JWT | List domains (filter: `subject_id`) | +| `POST` | `/api/domains` | JWT | Create domain | +| `PATCH` | `/api/domains/{id}` | JWT | Update domain | +| `DELETE` | `/api/domains/{id}` | JWT | Delete domain | +| `POST` | `/api/domains/{domainId}/ai-suggest` | JWT | AI-suggest topics for domain | +| `GET` | `/api/topics` | JWT | List topics (filter: `domain_id`, `subject_id`) | +| `POST` | `/api/topics` | JWT | Create topic | +| `PATCH` | `/api/topics/{id}` | JWT | Update topic | +| `DELETE` | `/api/topics/{id}` | JWT | Delete topic | + +--- + +## 17. Adaptive Learning Engine + +### 17.1 Models + +**`encoach.proficiency`** + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student user | +| `subject_id` | Many2one | Subject | +| `topic_id` | Many2one | Topic | +| `mastery_score` | Float | 0.0 to 1.0 | +| `confidence` | Float | Statistical confidence | +| `last_assessed` | Datetime | Last assessment time | +| `assessment_count` | Integer | Number of assessments | + +**`encoach.learning.plan`** + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `subject_id` | Many2one | Subject | +| `status` | Selection | `active`, `paused`, `completed` | +| `items` | One2many | Plan items (topics in order) | +| `generated_by` | Selection | `ai`, `manual` | +| `progress` | Float | Computed completion percentage | + +**`encoach.learning.plan.item`** + +| Field | Type | Description | +|-------|------|-------------| +| `plan_id` | Many2one | Parent plan | +| `topic_id` | Many2one | Topic | +| `sequence` | Integer | Order in plan | +| `status` | Selection | `locked`, `available`, `in_progress`, `completed` | +| `estimated_hours` | Float | Estimated time | +| `mastery_threshold` | Float | Required mastery to complete | + +**`encoach.diagnostic.session`** + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `subject_id` | Many2one | Subject | +| `status` | Selection | `in_progress`, `completed` | +| `questions_answered` | Integer | Count | +| `result_data` | Text (JSON) | Topic-level results | + +**`encoach.content.cache`** + +| Field | Type | Description | +|-------|------|-------------| +| `topic_id` | Many2one | Topic | +| `content_type` | Selection | `explanation`, `practice`, `quiz` | +| `content` | Text (JSON) | Cached AI content | +| `generated_at` | Datetime | Generation time | +| `ttl_hours` | Integer | Cache TTL | + +### 17.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/diagnostic/start` | JWT | Start diagnostic for subject | +| `POST` | `/api/diagnostic/answer` | JWT | Submit diagnostic answer (adaptive next question) | +| `GET` | `/api/diagnostic/{sessionId}/result` | JWT | Get diagnostic result | +| `GET` | `/api/proficiency` | JWT | Get proficiency records (filter: `subject_id`, `student_id`) | +| `GET` | `/api/proficiency/summary` | JWT | Aggregate proficiency summary | +| `GET` | `/api/proficiency/class` | JWT | Class-wide proficiency | +| `GET` | `/api/learning-plan` | JWT | Get learning plan (filter: `subject_id`) | +| `POST` | `/api/learning-plan/generate` | JWT | AI-generate learning plan | +| `PATCH` | `/api/learning-plan/{id}` | JWT | Update plan | +| `POST` | `/api/learning-plan/{id}/pause` | JWT | Pause plan | +| `POST` | `/api/learning-plan/{id}/resume` | JWT | Resume plan | +| `GET` | `/api/topics/{topicId}/content` | JWT | Get topic learning content | +| `POST` | `/api/topics/{topicId}/generate-content` | JWT | AI-generate topic content | +| `POST` | `/api/topics/{topicId}/practice` | JWT | Generate practice questions | +| `POST` | `/api/topics/{topicId}/practice/grade` | JWT | Grade practice answers | +| `POST` | `/api/topics/{topicId}/mastery-quiz` | JWT | Start mastery quiz | +| `POST` | `/api/topics/{topicId}/mastery-quiz/submit` | JWT | Submit mastery quiz and update proficiency | + +### 17.3 Diagnostic Algorithm + +Computer Adaptive Testing (CAT): +1. Start with medium-difficulty questions +2. If correct → harder question; if incorrect → easier question +3. Uses Item Response Theory (IRT) to estimate ability +4. Covers multiple topics within the subject +5. Terminates after sufficient confidence or max questions (20-30) +6. Produces per-topic mastery scores + +### 17.4 Learning Plan Generation + +AI-generated study plan: +1. Read student's proficiency profile +2. Identify weak topics (mastery < threshold) +3. Respect prerequisite graph (topological sort) +4. Estimate time per topic based on gap +5. Order topics: prerequisites first, then weakest areas +6. Return ordered list of `plan_items` + +--- + +## 18. Learning Resources + +### 18.1 Model (`encoach.resource`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Resource title | +| `type` | Selection | `video`, `document`, `link`, `interactive` | +| `subject_id` | Many2one | Subject | +| `topic_ids` | Many2many | Topics | +| `file` | Binary | Uploaded file | +| `url` | Char | External URL | +| `difficulty` | Selection | `beginner`, `intermediate`, `advanced` | +| `duration_minutes` | Integer | Estimated duration | +| `active` | Boolean | Soft-delete flag | + +### 18.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/resources` | JWT | Paginated resources (filter: `subject_id`, `topic_id`, `type`, `difficulty`) | +| `POST` | `/api/resources` | JWT | Upload resource (multipart) | +| `PATCH` | `/api/resources/{id}` | JWT | Update resource metadata | +| `DELETE` | `/api/resources/{id}` | JWT | Soft-delete resource | +| `POST` | `/api/resources/{id}/complete` | JWT | Mark resource as completed by student | +| `POST` | `/api/resources/{id}/rate` | JWT | Rate resource (1-5) | +| `GET` | `/api/resources/{id}/download` | JWT | Download resource file | + +--- + +## 19. AI Services + +### 19.1 AI Coaching + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/coach/chat` | JWT | General AI chat (context-aware) | +| `POST` | `/api/coach/hint` | JWT | Hint for topic/question | +| `POST` | `/api/coach/explain` | JWT | Explain scores or concepts | +| `POST` | `/api/coach/suggest` | JWT | Study suggestions | +| `POST` | `/api/coach/writing-help` | JWT | Writing feedback with improved text | +| `GET` | `/api/coach/tip` | JWT | Context-aware tip (FAISS similarity) | + +### 19.2 AI Evaluation + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/evaluate/writing` | JWT | Evaluate writing (IELTS band scoring / rubric-based) | +| `POST` | `/api/evaluate/speaking` | JWT | Evaluate speaking (audio → transcribe → grade) | +| `POST` | `/api/grading/multiple` | JWT | Grade multiple-choice / numerical answers | +| `POST` | `/api/transcribe` | JWT | Audio transcription (Whisper, multipart upload) | + +### 19.3 AI Generation + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/exam/{module}/generate` | JWT | Generate exam exercises from template | +| `POST` | `/api/exam/{module}/generate/scratch` | JWT | Generate full exam from scratch | + +### 19.4 Media Generation + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/exam/listening/media` | JWT | Generate listening audio (AWS Polly TTS) | +| `POST` | `/api/exam/speaking/media` | JWT | Generate speaking video (ELAI) | +| `GET` | `/api/exam/avatars` | JWT | List ELAI avatars with voice metadata | + +### 19.5 AI Analytics + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/ai/search` | JWT | AI-powered semantic search | +| `POST` | `/api/ai/insights` | JWT | AI-generated insights from data | +| `GET` | `/api/ai/alerts` | JWT | AI-detected alerts (at-risk students, anomalies) | +| `POST` | `/api/ai/report-narrative` | JWT | AI narrative for reports | +| `POST` | `/api/ai/batch-optimize` | JWT | Batch optimization suggestions | +| `POST` | `/api/ai/grade-suggest` | JWT | AI grading suggestion | + +### 19.6 AI Service Configuration + +All AI services read configuration from `ir.config_parameter`: + +| Parameter Key | Description | Default | +|--------------|-------------|---------| +| `encoach.openai_api_key` | OpenAI API key | Required | +| `encoach.openai_model` | Default model | `gpt-4o` | +| `encoach.openai_model_light` | Light model for simple tasks | `gpt-3.5-turbo` | +| `encoach.aws_access_key_id` | AWS access key | Required | +| `encoach.aws_secret_access_key` | AWS secret key | Required | +| `encoach.aws_region` | AWS region | `eu-west-1` | +| `encoach.polly_voice_id` | Default Polly voice | `Joanna` | +| `encoach.polly_engine` | Polly engine | `neural` | +| `encoach.whisper_model` | Whisper model size | `base` | +| `encoach.whisper_workers` | Whisper worker threads | `4` | +| `encoach.elai_api_key` | ELAI API key | Required | +| `encoach.gptzero_api_key` | GPTZero API key | Required | +| `encoach.faiss_embedding_model` | Sentence transformer model | `all-MiniLM-L6-v2` | + +--- + +## 20. Training Content + +### 20.1 Models + +**`encoach.training.tip`** + +| Field | Type | Description | +|-------|------|-------------| +| `title` | Char | Tip title | +| `content` | Text | Tip content | +| `subject_id` | Many2one | Subject | +| `module` | Char | Module tag | +| `embedding` | Binary(attachment=False) | FAISS embedding vector | + +**`encoach.training.walkthrough`** + +| Field | Type | Description | +|-------|------|-------------| +| `title` | Char | Walkthrough title | +| `steps` | Text (JSON) | Step-by-step content | +| `module` | Char | Module tag | + +### 20.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/training` | JWT | Training records (filter: `entity_id`, `user_id`, `module`) | +| `GET` | `/api/training/tips` | JWT | Training tips | +| `GET` | `/api/training/walkthrough` | JWT | Walkthroughs (filter: `module`) | + +--- + +## 21. Analytics and Statistics + +### 21.1 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/sessions` | JWT | Exam sessions | +| `GET` | `/api/stats` | JWT | Exam statistics | +| `GET` | `/api/statistical` | JWT | Aggregated statistical data | +| `GET` | `/api/stats/performance` | JWT | Performance metrics | +| `GET` | `/api/analytics/student` | JWT | Student analytics (filter: `student_id`, `subject_id`, `date_range`) | +| `GET` | `/api/analytics/class` | JWT | Class analytics (filter: `batch_id`, `course_id`) | +| `GET` | `/api/analytics/subject` | JWT | Subject analytics (filter: `subject_id`) | +| `GET` | `/api/analytics/content-gaps` | JWT | Content gap analysis | + +--- + +## 22. Subscription and Payments + +### 22.1 Models + +**`encoach.package`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Package name | +| `description` | Text | Description | +| `price` | Float | Price | +| `currency` | Char | Currency code | +| `duration_days` | Integer | Access duration | +| `subject_ids` | Many2many | Included subjects | +| `features` | Text (JSON) | Feature list | +| `active` | Boolean | Available for purchase | + +**`encoach.payment`** + +| Field | Type | Description | +|-------|------|-------------| +| `user_id` | Many2one | Paying user | +| `entity_id` | Many2one | Entity (if institutional) | +| `package_id` | Many2one | Package | +| `amount` | Float | Payment amount | +| `currency` | Char | Currency | +| `provider` | Selection | `stripe`, `paypal`, `paymob`, `invoice` | +| `status` | Selection | `pending`, `completed`, `failed`, `refunded` | +| `provider_ref` | Char | External reference | +| `created_at` | Datetime | Payment date | + +**`encoach.discount`** + +| Field | Type | Description | +|-------|------|-------------| +| `code` | Char | Discount code | +| `percentage` | Float | Discount percentage | +| `valid_from` | Datetime | Start date | +| `valid_to` | Datetime | End date | +| `max_uses` | Integer | Maximum uses | +| `current_uses` | Integer | Current use count | + +### 22.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/packages` | JWT | List packages | +| `GET` | `/api/payments` | JWT | Paginated payments | +| `POST` | `/api/stripe/checkout` | JWT | Create Stripe checkout session, return URL | +| `POST` | `/api/paypal/checkout` | JWT | Create PayPal order, return approval URL | +| `POST` | `/api/paymob/checkout` | JWT | Create Paymob payment, return URL | +| `GET` | `/api/discounts` | JWT | List discounts | +| `POST` | `/api/discounts` | JWT | Create discount | +| `DELETE` | `/api/discounts/{id}` | JWT | Delete discount | + +### 22.3 Webhook Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/stripe/webhook` | Webhook signature | Stripe payment events | +| `POST` | `/api/paypal/webhook` | Webhook signature | PayPal payment events | +| `POST` | `/api/paymob/webhook` | Webhook HMAC | Paymob transaction callback | + +--- + +## 23. Support Tickets + +### 23.1 Model (`encoach.ticket`) + +| Field | Type | Description | +|-------|------|-------------| +| `subject` | Char | Ticket subject | +| `description` | Text | Description | +| `type` | Selection | `bug`, `feature`, `support`, `feedback` | +| `status` | Selection | `open`, `in_progress`, `resolved`, `closed` | +| `reporter_id` | Many2one | Reporter user | +| `assignee_id` | Many2one | Assigned staff | +| `entity_id` | Many2one | Entity context | +| `source` | Selection | `platform`, `webpage` | + +### 23.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/tickets` | JWT | Paginated tickets (filter: `status`, `type`, `assignee_id`, `source`) | +| `POST` | `/api/tickets` | JWT | Create ticket | +| `PATCH` | `/api/tickets/{id}` | JWT | Update ticket | +| `GET` | `/api/tickets/assignedToUser` | JWT | Tickets assigned to current user | + +--- + +## 24. File Storage + +### 24.1 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/storage/upload` | JWT | Upload file (multipart/form-data), return attachment ID and URL | +| `GET` | `/api/storage/url` | JWT | Resolve download URL for filename | + +Files are stored as `ir.attachment` records. For production, configure Odoo to use S3-compatible storage. + +--- + +## 25. Approval Workflows (Enhanced) + +### 25.1 Model (`encoach.approval.workflow`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Workflow name | +| `type` | Selection | `exam_publish`, `assignment_publish`, `content_publish`, `material_publish` | +| `stage_ids` | One2many | Approval stages (ordered) | +| `entity_id` | Many2one | Entity | +| `allow_bypass` | Boolean | Whether bypass is allowed | +| `active` | Boolean | Active flag | + +### 25.2 Model (`encoach.approval.stage`) + +| Field | Type | Description | +|-------|------|-------------| +| `workflow_id` | Many2one | Parent workflow | +| `sequence` | Integer | Stage order | +| `approver_id` | Many2one | Assigned approver (`res.users`) | +| `max_days` | Integer | Maximum days before auto-escalation | +| `auto_escalate` | Boolean | Whether to auto-escalate on timeout | +| `notification_email` | Char | Email for stage notifications | +| `status` | Selection | `pending`, `approved`, `rejected`, `escalated`, `bypassed` | +| `comment` | Text | Reviewer comment | +| `acted_at` | Datetime | When action was taken | + +### 25.3 Model (`encoach.approval.request`) + +| Field | Type | Description | +|-------|------|-------------| +| `workflow_id` | Many2one | Workflow template | +| `res_model` | Char | Target model (e.g., `encoach.exam`) | +| `res_id` | Integer | Target record ID | +| `current_stage_id` | Many2one | Current active stage | +| `requester_id` | Many2one | User who submitted for approval | +| `state` | Selection | `in_progress`, `approved`, `rejected`, `bypassed` | +| `bypass_reason` | Text | Justification if bypassed | +| `created_at` | Datetime | Request creation time | + +### 25.4 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/approval-workflows` | JWT | Paginated workflows | +| `POST` | `/api/approval-workflows` | JWT | Create workflow with stages | +| `GET` | `/api/approval-workflows/{id}` | JWT | Get workflow with stages | +| `PATCH` | `/api/approval-workflows/{id}` | JWT | Update workflow and stages | +| `DELETE` | `/api/approval-workflows/{id}` | JWT | Delete workflow | +| `POST` | `/api/approval-requests` | JWT | Submit item for approval | +| `GET` | `/api/approval-requests` | JWT | List approval requests (filter: state, type) | +| `POST` | `/api/approval-requests/{id}/approve` | JWT | Approve current stage | +| `POST` | `/api/approval-requests/{id}/reject` | JWT | Reject at current stage | +| `POST` | `/api/approval-requests/{id}/bypass` | JWT | Bypass approval (requires justification) | +| `POST` | `/api/approval-requests/{id}/return` | JWT | Return to specific stage | + +### 25.5 Cron: Auto-Escalation + +A server cron runs daily to check approval stages that have exceeded `max_days`. For stages with `auto_escalate = True`, the system automatically advances to the next stage and sends a notification email to the new approver with escalation reason. + +--- + +## 26. Courseware and Content Delivery + +**Module:** `encoach_courseware` +**Dependencies:** `encoach_core`, `openeducat_core`, `encoach_ai` + +### 26.1 Model (`encoach.course.chapter`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char (required) | Chapter title | +| `course_id` | Many2one (`op.course`) | Parent course | +| `sequence` | Integer | Display order | +| `description` | Text | Chapter description | +| `start_date` | Datetime | Scheduled start date | +| `end_date` | Datetime | Optional end date | +| `unlock_mode` | Selection | `auto_date`, `manual`, `prerequisite` | +| `is_unlocked` | Boolean | Current unlock status | +| `topic_id` | Many2one (`encoach.topic`) | Bridge to adaptive engine | +| `material_ids` | One2many | Chapter materials | +| `assignment_ids` | Many2many (`encoach.assignment`) | Linked assignments | +| `exam_ids` | Many2many (`encoach.exam`) | Linked exams/quizzes | +| `active` | Boolean | Soft-delete flag | + +### 26.2 Model (`encoach.chapter.material`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char (required) | Material title | +| `chapter_id` | Many2one | Parent chapter | +| `type` | Selection | `pdf`, `document`, `video`, `audio`, `image`, `link`, `article` | +| `file` | Binary (attachment=True) | Uploaded file | +| `url` | Char | External URL | +| `description` | Text | Description | +| `sequence` | Integer | Display order | +| `allow_download` | Boolean | Student download permission | +| `is_book` | Boolean | Course book flag | +| `book_chapters` | Text | JSON book chapter structure | +| `active` | Boolean | Soft-delete flag | + +### 26.3 Model (`encoach.chapter.progress`) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one (`res.users`) | Student | +| `chapter_id` | Many2one | Chapter | +| `status` | Selection | `not_started`, `in_progress`, `completed` | +| `started_at` | Datetime | First access time | +| `completed_at` | Datetime | Completion time | +| `materials_completed` | Integer | Count of materials viewed | +| `materials_total` | Integer | Computed total materials | + +### 26.4 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/courses/{courseId}/chapters` | JWT | List chapters (ordered by sequence) | +| `POST` | `/api/courses/{courseId}/chapters` | JWT (teacher) | Create chapter | +| `GET` | `/api/chapters/{id}` | JWT | Chapter detail with material count | +| `PATCH` | `/api/chapters/{id}` | JWT (teacher) | Update chapter | +| `DELETE` | `/api/chapters/{id}` | JWT (teacher) | Soft-delete chapter | +| `POST` | `/api/chapters/{id}/unlock` | JWT (teacher) | Manually unlock | +| `POST` | `/api/chapters/{id}/lock` | JWT (teacher) | Manually lock | +| `PATCH` | `/api/courses/{courseId}/chapters/reorder` | JWT (teacher) | Reorder (body: `{ids: []}`) | +| `GET` | `/api/chapters/{id}/progress` | JWT | Student progress | +| `POST` | `/api/chapters/{id}/progress/complete` | JWT (student) | Mark chapter completed | +| `GET` | `/api/chapters/{chapterId}/materials` | JWT | List materials | +| `POST` | `/api/chapters/{chapterId}/materials` | JWT (teacher) | Upload material (multipart) | +| `PATCH` | `/api/materials/{id}` | JWT (teacher) | Update material metadata | +| `DELETE` | `/api/materials/{id}` | JWT (teacher) | Delete material | +| `GET` | `/api/materials/{id}/download` | JWT | Download file (streaming response) | +| `POST` | `/api/materials/{id}/viewed` | JWT (student) | Record material view | +| `PATCH` | `/api/chapters/{chapterId}/materials/reorder` | JWT (teacher) | Reorder materials | + +### 26.5 AI Workbench Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/workbench/generate-outline` | JWT (teacher) | AI generates course outline from requirements (topic, objectives, complexity) | +| `POST` | `/api/workbench/generate-chapter` | JWT (teacher) | AI generates content for a specific chapter | +| `POST` | `/api/workbench/generate-rubric` | JWT (teacher) | AI generates grading rubric | +| `POST` | `/api/workbench/regenerate` | JWT (teacher) | Regenerate specific section | +| `POST` | `/api/workbench/suggest-materials` | JWT (teacher) | AI suggests supplementary materials based on student performance profiles | +| `POST` | `/api/workbench/publish` | JWT (teacher) | Publish generated content to course chapters (may trigger approval workflow) | + +AI Workbench uses GPT-4o (temp 0.7 for generation) and calls `encoach_ai` service layer. + +### 26.6 Cron: Auto-Unlock Chapters + +Server cron runs every hour. For chapters with `unlock_mode = 'auto_date'` and `start_date <= now()` and `is_unlocked = False`, set `is_unlocked = True` and trigger notification. + +--- + +## 27. Communication System + +**Module:** `encoach_communication` +**Dependencies:** `encoach_core`, `openeducat_core` + +### 27.1 Model (`encoach.discussion.board`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char (required) | Board title | +| `course_id` | Many2one (`op.course`) | Associated course | +| `batch_id` | Many2one (`op.batch`) | Associated batch (optional) | +| `chapter_id` | Many2one (`encoach.course.chapter`) | Associated chapter (optional) | +| `assignment_id` | Many2one | Associated assignment (optional) | +| `is_enabled` | Boolean | Teacher toggle | +| `allow_student_posts` | Boolean | Student thread creation permission | +| `post_count` | Integer | Computed count of posts | + +### 27.2 Model (`encoach.discussion.post`) + +| Field | Type | Description | +|-------|------|-------------| +| `board_id` | Many2one | Parent board | +| `parent_id` | Many2one (self) | Parent post for threading | +| `author_id` | Many2one (`res.users`) | Author | +| `title` | Char | Thread title (root posts only) | +| `content` | Text | Post body (markdown) | +| `attachment_ids` | Many2many (`ir.attachment`) | File attachments | +| `is_pinned` | Boolean | Teacher pin | +| `is_resolved` | Boolean | Resolved flag (Q&A mode) | +| `reply_count` | Integer | Computed child count | +| `created_at` | Datetime | Post timestamp | + +### 27.3 Model (`encoach.announcement`) + +| Field | Type | Description | +|-------|------|-------------| +| `title` | Char (required) | Announcement title | +| `content` | Text | Body (markdown) | +| `author_id` | Many2one (`res.users`) | Author | +| `course_id` | Many2one (`op.course`) | Target course (null = system-wide) | +| `batch_id` | Many2one (`op.batch`) | Target batch (null = all batches) | +| `priority` | Selection | `normal`, `important`, `urgent` | +| `is_published` | Boolean | Published status | +| `published_at` | Datetime | Publish timestamp | +| `expires_at` | Datetime | Expiry time | +| `send_email` | Boolean | Also send via email | +| `attachment_ids` | Many2many (`ir.attachment`) | Attachments | + +### 27.4 Model (`encoach.message`) + +| Field | Type | Description | +|-------|------|-------------| +| `sender_id` | Many2one (`res.users`) | Sender | +| `recipient_id` | Many2one (`res.users`) | Recipient | +| `subject` | Char | Subject line | +| `content` | Text | Message body | +| `is_read` | Boolean | Read flag | +| `read_at` | Datetime | Read timestamp | +| `attachment_ids` | Many2many (`ir.attachment`) | Attachments | +| `send_email_copy` | Boolean | Send email copy | +| `created_at` | Datetime | Sent time | + +### 27.5 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/discussion-boards` | JWT | List boards (filter: `course_id`, `batch_id`) | +| `POST` | `/api/discussion-boards` | JWT (teacher) | Create board | +| `PATCH` | `/api/discussion-boards/{id}` | JWT (teacher) | Update settings | +| `GET` | `/api/discussion-boards/{id}/posts` | JWT | Paginated posts (threaded) | +| `POST` | `/api/discussion-boards/{id}/posts` | JWT | Create post/reply | +| `PATCH` | `/api/posts/{id}` | JWT | Update own post | +| `DELETE` | `/api/posts/{id}` | JWT (teacher/admin) | Delete post | +| `POST` | `/api/posts/{id}/pin` | JWT (teacher) | Pin/unpin toggle | +| `POST` | `/api/posts/{id}/resolve` | JWT (teacher) | Mark as resolved | +| `GET` | `/api/announcements` | JWT | List announcements (filter: `course_id`, `priority`) | +| `POST` | `/api/announcements` | JWT (teacher/admin) | Create announcement | +| `PATCH` | `/api/announcements/{id}` | JWT | Update | +| `DELETE` | `/api/announcements/{id}` | JWT | Delete | +| `POST` | `/api/announcements/{id}/publish` | JWT | Publish | +| `GET` | `/api/messages` | JWT | Inbox (filter: `is_read`) | +| `GET` | `/api/messages/sent` | JWT | Sent messages | +| `POST` | `/api/messages` | JWT | Send message | +| `GET` | `/api/messages/{id}` | JWT | Message detail (auto-marks read) | +| `DELETE` | `/api/messages/{id}` | JWT | Delete message | +| `GET` | `/api/messages/unread-count` | JWT | Unread count `{count: N}` | + +### 27.6 Email Integration + +When `send_email = True` on announcements or `send_email_copy = True` on messages, the module uses Odoo's `mail.thread` mixin to send emails through the configured outgoing mail server. + +--- + +## 28. Notification Engine + +**Module:** `encoach_notification` +**Dependencies:** `encoach_core` + +### 28.1 Model (`encoach.notification`) + +| Field | Type | Description | +|-------|------|-------------| +| `user_id` | Many2one (`res.users`) | Recipient | +| `title` | Char | Notification title | +| `message` | Text | Body | +| `type` | Selection | `deadline`, `chapter_unlock`, `result_release`, `announcement`, `assignment`, `exam`, `message`, `system` | +| `action_url` | Char | Frontend URL to navigate to | +| `is_read` | Boolean | Read status | +| `read_at` | Datetime | Read timestamp | +| `channel` | Selection | `in_app`, `email`, `both` | +| `created_at` | Datetime | Created time | + +### 28.2 Model (`encoach.notification.rule`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Rule name | +| `event_type` | Selection | `assignment_due`, `exam_due`, `chapter_unlock`, `result_release`, `submission_graded`, `extension_response` | +| `days_before` | Integer | Days before event to trigger | +| `frequency` | Selection | `once`, `daily`, `custom` | +| `custom_intervals` | Text (JSON) | Custom intervals array (e.g., `[7, 3, 1]`) | +| `channel` | Selection | `in_app`, `email`, `both` | +| `entity_id` | Many2one | Entity scope | +| `active` | Boolean | Active flag | + +### 28.3 Model (`encoach.notification.preferences`) + +| Field | Type | Description | +|-------|------|-------------| +| `user_id` | Many2one (`res.users`) | One per user | +| `email_enabled` | Boolean | Master email toggle | +| `assignment_alerts` | Boolean | Assignment notifications | +| `exam_alerts` | Boolean | Exam notifications | +| `chapter_alerts` | Boolean | Chapter unlock notifications | +| `announcement_alerts` | Boolean | Announcement notifications | +| `message_alerts` | Boolean | Message notifications | + +### 28.4 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/notifications` | JWT | Paginated list (filter: `type`, `is_read`) | +| `POST` | `/api/notifications/{id}/read` | JWT | Mark as read | +| `POST` | `/api/notifications/read-all` | JWT | Mark all as read | +| `GET` | `/api/notifications/unread-count` | JWT | `{count: N}` | +| `GET` | `/api/notification-rules` | JWT (admin) | List rules | +| `POST` | `/api/notification-rules` | JWT (admin) | Create rule | +| `PATCH` | `/api/notification-rules/{id}` | JWT (admin) | Update rule | +| `DELETE` | `/api/notification-rules/{id}` | JWT (admin) | Delete rule | +| `GET` | `/api/notification-preferences` | JWT | Get user preferences | +| `PATCH` | `/api/notification-preferences` | JWT | Update preferences | + +### 28.5 Crons + +1. **Deadline Reminders:** Runs daily. Checks all active rules, matches upcoming events (assignments, exams) within `days_before`, creates notifications and sends emails per user preferences. +2. **Chapter Unlock Notifications:** Triggered by the courseware cron when chapters are auto-unlocked. + +--- + +## 29. FAQ System + +**Module:** `encoach_faq` +**Dependencies:** `encoach_core` + +### 29.1 Model (`encoach.faq.category`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Category name | +| `sequence` | Integer | Display order | +| `icon` | Char | Icon identifier | +| `audience` | Selection | `student`, `entity`, `both` | +| `active` | Boolean | Active flag | + +### 29.2 Model (`encoach.faq.item`) + +| Field | Type | Description | +|-------|------|-------------| +| `question` | Char | Question text | +| `answer` | Text | Answer (markdown, embedded images/video) | +| `category_id` | Many2one | Parent category | +| `audience` | Selection | `student`, `entity`, `both` | +| `image` | Binary | Optional image | +| `video_url` | Char | Optional video URL | +| `sequence` | Integer | Display order | +| `active` | Boolean | Active flag | + +### 29.3 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/faq/categories` | Public/JWT | List categories (filter: `audience`) | +| `POST` | `/api/faq/categories` | JWT (admin) | Create category | +| `PATCH` | `/api/faq/categories/{id}` | JWT (admin) | Update category | +| `DELETE` | `/api/faq/categories/{id}` | JWT (admin) | Delete category | +| `GET` | `/api/faq/items` | Public/JWT | List items (filter: `category_id`, `audience`, `search`) | +| `POST` | `/api/faq/items` | JWT (admin) | Create item | +| `PATCH` | `/api/faq/items/{id}` | JWT (admin) | Update item | +| `DELETE` | `/api/faq/items/{id}` | JWT (admin) | Delete item | + +--- + +## 30. Plagiarism Detection + +**Module enhancement:** `encoach_ai_grading` (add plagiarism sub-service) + +### 30.1 Plagiarism Report Storage + +Add to `encoach.submission` or create `encoach.plagiarism.report`: + +| Field | Type | Description | +|-------|------|-------------| +| `submission_id` | Many2one | Target submission | +| `overall_score` | Float | AI probability score (0-1) | +| `per_sentence` | Text (JSON) | Per-sentence analysis from GPTZero | +| `report_pdf` | Binary | Generated PDF report | +| `checked_at` | Datetime | When check was run | + +### 30.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/plagiarism/check` | JWT (teacher) | Submit text for GPTZero analysis | +| `GET` | `/api/plagiarism/report/{submissionId}` | JWT | Get plagiarism report | +| `GET` | `/api/plagiarism/report/{submissionId}/download` | JWT | Download PDF report | + +### 30.3 Integration + +- Uses GPTZero `v2/predict/text` endpoint (already configured in `encoach_ai_grading`) +- Can be enabled/disabled per assignment via `plagiarism_enabled` boolean on `encoach.assignment` +- Configurable max submissions for plagiarism check via `plagiarism_max_checks` integer + +--- + +## 31. Official Exam Access + +**Module enhancement:** `encoach_exam` (add official access modes) + +### 31.1 Model Extensions + +Add to `encoach.exam`: + +| Field | Type | Description | +|-------|------|-------------| +| `is_exercise` | Boolean | Exercise (practice, no journey tracking) vs Exam | +| `is_official` | Boolean | Official exam flag | +| `access_mode` | Selection | `normal`, `link`, `landing_page`, `separate_login` | +| `access_token` | Char | Unique access token (auto-generated) | +| `access_code` | Char | Exam access code | +| `activation_start` | Datetime | Exam start time | +| `activation_end` | Datetime | Exam end time | +| `generate_unique_per_student` | Boolean | AI generates unique variant per student | +| `reminder_count` | Integer | Number of reminders to send | +| `reminder_frequency` | Selection | `once`, `daily`, `custom` | + +### 31.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `POST` | `/api/exam/{id}/generate-link` | JWT (teacher) | Generate unique access token/URL | +| `GET` | `/api/exam/access/{token}` | Public | Access exam via token (validates token, returns exam info) | +| `POST` | `/api/exam/official-login` | Public | Login with exam code + credentials | +| `POST` | `/api/exam/{id}/suspend` | JWT (teacher) | Suspend exam | +| `POST` | `/api/exam/{id}/revoke` | JWT (teacher) | Revoke exam access | + +--- + +## 32. Non-Functional Requirements + +### 32.1 Performance + +| ID | Requirement | +|----|-------------| +| NFR-PERF-01 | API response time < 500ms for CRUD endpoints | +| NFR-PERF-02 | AI endpoints may take up to 30s (use async job for long operations) | +| NFR-PERF-03 | Pagination: default `size=20`, max `size=100` | + +### 32.2 Security + +| ID | Requirement | +|----|-------------| +| NFR-SEC-01 | JWT tokens expire after 24 hours | +| NFR-SEC-02 | All endpoints validate entity-scoped permissions | +| NFR-SEC-03 | Rate limiting on AI endpoints (configurable) | +| NFR-SEC-04 | CORS configured for frontend origin | +| NFR-SEC-05 | File upload size limit: 50MB | +| NFR-SEC-06 | API keys stored in `ir.config_parameter`, never in code | + +### 32.3 Scalability + +| ID | Requirement | +|----|-------------| +| NFR-SCALE-01 | Support 10,000+ concurrent students | +| NFR-SCALE-02 | Support 50+ subjects, 500+ topics per subject, 1000+ resources per subject | +| NFR-SCALE-03 | Database indices on all foreign keys and frequently filtered fields | + +### 32.4 Reliability + +| ID | Requirement | +|----|-------------| +| NFR-REL-01 | AI service failures return graceful error (never 500) | +| NFR-REL-02 | Webhook processing is idempotent | +| NFR-REL-03 | Soft-delete for resources (preserve completion records) | + +### 32.5 API Conventions + +| Convention | Detail | +|-----------|--------| +| Date format | ISO 8601 (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) | +| Pagination | `?page=1&size=20` returns `{ "items": [...], "total": N, "page": 1, "size": 20 }` | +| Error shape | `{ "error": "message", "code": "ERROR_CODE" }` with HTTP status | +| ID fields | Many2one serialized as `{field}_id` (int) + `{field}_name` (str) | +| List fields | Many2many serialized as `{field}_ids` (int[]) + `{field}_names` (str[]) | +| Binary fields | Base64-encoded strings or separate download endpoint | +| Auth header | `Authorization: Bearer ` | + +--- + +## 33. Implementation Status + +All modules have been implemented and deployed. The original priority ordering is preserved for reference. + +| Priority | Modules | Status | +|----------|---------|--------| +| **P0** | `encoach_core`, `encoach_api` (auth, users, entities, permissions) | **DONE** | +| **P0** | `openeducat_core` (port to v19), `encoach_lms_api` | **DONE** | +| **P1** | `encoach_taxonomy`, `encoach_adaptive`, `encoach_adaptive_api`, `encoach_adaptive_ai` | **DONE** | +| **P1** | `encoach_exam`, `encoach_ai_generation`, `encoach_ai_grading` | **DONE** | +| **P1** | `encoach_resources` | **DONE** | +| **P2** | `encoach_courseware` (chapters, materials, progress, AI workbench) | **DONE** | +| **P2** | `encoach_communication` (discussions, announcements, messaging) | **DONE** | +| **P2** | `openeducat_timetable`, `openeducat_attendance`, `openeducat_classroom` | **DONE** | +| **P2** | `openeducat_admission` + admission endpoints | **DONE** | +| **P2** | Academic years/terms, departments endpoints | **DONE** | +| **P2** | `encoach_ai`, `encoach_ai_media` | **DONE** | +| **P2** | `encoach_assignment`, `encoach_classroom` | **DONE** | +| **P3** | `encoach_notification` (rules, email delivery, preferences) | **DONE** | +| **P3** | Enhanced approval workflows (stages, escalation, bypass) | **DONE** | +| **P3** | Plagiarism detection (GPTZero integration) | **DONE** | +| **P3** | Official exam access modes (link, landing, separate login) | **DONE** | +| **P3** | Late submission handling, extension requests | **DONE** | +| **P3** | `openeducat_exam` + institutional exam endpoints | **DONE** | +| **P3** | `openeducat_assignment` + submission endpoints | **DONE** | +| **P3** | Subject registration endpoints | **DONE** | +| **P3** | `encoach_training`, analytics endpoints | **DONE** | +| **P3** | `encoach_sis`, `encoach_branding` | **DONE** | +| **P4** | `encoach_faq` (categories, items, role-filtered) | **DONE** | +| **P4** | `encoach_subscription` (Stripe, PayPal, Paymob) | **DONE** | +| **P4** | `encoach_ticket` | **DONE** | +| **P4** | Storage | **DONE** | + +--- + +## 34. Beyond-SRS Features (Implemented) + +The developer implemented the following features beyond the original SRS scope. These leverage OpenEduCat Enterprise modules and add significant institutional management capabilities. + +### 34.1 Student Leave Management + +**Module:** `openeducat_student_leave_enterprise` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/student_leave.py` +**Frontend:** `student-leave.service.ts` | `AdminStudentLeave.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/student-leaves` | List leave requests | +| `POST` | `/api/student-leaves` | Create leave request | +| `PATCH` | `/api/student-leaves/{id}` | Update leave request | +| `POST` | `/api/student-leaves/{id}/approve` | Approve request | +| `POST` | `/api/student-leaves/{id}/reject` | Reject request | +| `GET` | `/api/student-leave-types` | List leave types | +| `POST` | `/api/student-leave-types` | Create leave type | + +### 34.2 Fees Management + +**Module:** `openeducat_fees` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/fees.py` +**Frontend:** `fees.service.ts` | `AdminFees.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/fees-plans` | List fee plans | +| `GET` | `/api/fees-plans/{id}` | Fee plan detail | +| `GET` | `/api/student-fees` | Student fee details | +| `GET` | `/api/fees-terms` | Fee terms | + +### 34.3 Lessons Management + +**Module:** `openeducat_lesson` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/lessons.py` +**Frontend:** `lesson.service.ts` | `AdminLessons.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/lessons` | List lessons | +| `POST` | `/api/lessons` | Create lesson | +| `PATCH` | `/api/lessons/{id}` | Update lesson | +| `DELETE` | `/api/lessons/{id}` | Delete lesson | + +### 34.4 Gradebook and Grading Assignments + +**Module:** `openeducat_grading` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/gradebook.py` +**Frontend:** `gradebook.service.ts` | `AdminGradebook.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/gradebooks` | List gradebooks | +| `GET` | `/api/gradebook-lines` | Gradebook lines | +| `GET` | `/api/grading-assignments` | List grading assignments | +| `POST` | `/api/grading-assignments` | Create grading assignment | +| `PATCH` | `/api/grading-assignments/{id}` | Update grading assignment | +| `DELETE` | `/api/grading-assignments/{id}` | Delete grading assignment | + +### 34.5 Student Progress Tracking + +**Module:** `openeducat_student_progress_enterprise` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/student_progress.py` +**Frontend:** `student-progress.service.ts` | `AdminStudentProgress.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/student-progress` | List student progression records | + +### 34.6 Library Management + +**Module:** `openeducat_library` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/library.py` +**Frontend:** `library.service.ts` | `AdminLibrary.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/library/media` | List media items | +| `POST` | `/api/library/media` | Create media item | +| `DELETE` | `/api/library/media/{id}` | Delete media item | +| `GET` | `/api/library/movements` | List movements | +| `POST` | `/api/library/movements` | Create movement (issue) | +| `PATCH` | `/api/library/movements/{id}` | Update movement | +| `POST` | `/api/library/movements/{id}/return` | Return media | +| `GET` | `/api/library/cards` | List library cards | +| `POST` | `/api/library/cards` | Create library card | + +### 34.7 Activity Management + +**Module:** `openeducat_activity` (via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/activities.py` +**Frontend:** `activity.service.ts` | `AdminActivities.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/activities` | List activities | +| `POST` | `/api/activities` | Create activity | +| `PATCH` | `/api/activities/{id}` | Update activity | +| `DELETE` | `/api/activities/{id}` | Delete activity | +| `GET` | `/api/activity-types` | List activity types | +| `POST` | `/api/activity-types` | Create activity type | +| `DELETE` | `/api/activity-types/{id}` | Delete activity type | + +### 34.8 Facility and Asset Management + +**Module:** `openeducat_facility` (extended via `encoach_lms_api`) +**Controller:** `encoach_lms_api/controllers/facilities.py` +**Frontend:** `facility.service.ts` | `AdminFacilities.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/facilities` | List facilities | +| `POST` | `/api/facilities` | Create facility | +| `PATCH` | `/api/facilities/{id}` | Update facility | +| `DELETE` | `/api/facilities/{id}` | Delete facility | +| `GET` | `/api/assets` | List assets | +| `POST` | `/api/assets` | Create asset | +| `DELETE` | `/api/assets/{id}` | Delete asset | + +### 34.9 Roles CRUD, User Role Assignment, and Authority Matrix + +**Module:** `encoach_core` (extended) +**Controller:** `encoach_api/controllers/roles.py` +**Frontend:** `entities.service.ts` | `RolesPermissions.tsx`, `UserRoles.tsx`, `AuthorityMatrix.tsx` + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/roles` | List all roles | +| `POST` | `/api/roles` | Create role | +| `GET` | `/api/roles/{id}` | Get role detail | +| `PATCH` | `/api/roles/{id}` | Update role | +| `DELETE` | `/api/roles/{id}` | Delete role | +| `PATCH` | `/api/roles/{id}/permissions` | Update role permissions | +| `GET` | `/api/permissions` | List all permissions | +| `POST` | `/api/permissions` | Create permission | +| `DELETE` | `/api/permissions/{id}` | Delete permission | +| `GET` | `/api/users/with-roles` | List users with roles | +| `GET/PATCH` | `/api/users/{id}/roles` | Get/update user roles | +| `POST` | `/api/users/{id}/roles/toggle` | Toggle role for user | +| `GET` | `/api/authority-matrix` | Get role-permission matrix | +| `POST` | `/api/authority-matrix/toggle` | Toggle a matrix cell | +| `GET` | `/api/user-authority-matrix` | Get user-level authority | + +--- + +## Appendix A: Complete Endpoint Summary (Verified Against Deployment) + +Total API endpoints: **~377** (deployed across 4 controller packages) + +| Controller Package | Route Count | Service Areas | +|-------------------|-------------|---------------| +| `encoach_api` | ~120 | Auth, users, entities, roles, exams, assignments, classrooms, storage, stats, subscriptions, tickets, approvals, training, generation, AI analytics | +| `encoach_lms_api` | ~209 | Courses, students, teachers, batches, timetable, attendance, grades, departments, academic, admissions, subject registration, institutional exams, course assignments, student leave, fees, lessons, gradebook, student progress, library, activities, facilities, notifications, FAQ, courseware, plagiarism, communication, classrooms | +| `encoach_adaptive_api` | ~41 | Taxonomy (subjects, domains, topics), diagnostic, proficiency, learning plans, content, coaching | +| `encoach_resources` | ~7 | Learning resources (CRUD, download, complete, rate) | + +**Breakdown by Service Area:** + +| Service Area | Endpoint Count | Status | +|-------------|---------------|--------| +| Auth & Users | 10 | Deployed | +| Entities & Permissions | 9 | Deployed | +| Roles CRUD & Authority Matrix | 12 | **Deployed (Beyond SRS)** | +| LMS (Courses, Batches, Timetable, Attendance, Grades) | 16 | Deployed | +| Academic Years, Terms, Departments | 14 | Deployed | +| Admissions | 12 | Deployed | +| Subject Registration | 6 | Deployed | +| EnCoach Exams (AI) | 15 | Deployed | +| Institutional Exams | 21 | Deployed | +| EnCoach Assignments | 7 | Deployed | +| Course Assignments & Submissions | 11 | Deployed | +| Classrooms & Groups | 7 | Deployed | +| Taxonomy | 16 | Deployed | +| Adaptive Learning | 17 | Deployed | +| Resources | 7 | Deployed | +| AI Coaching | 6 | Deployed | +| AI Evaluation | 4 | Deployed | +| AI Generation | 2 | Deployed | +| AI Media | 3 | Deployed | +| AI Analytics | 6 | Deployed | +| Training | 3 | Deployed | +| Statistics | 4 | Deployed | +| Subscriptions & Payments | 8 + 3 webhooks | Deployed | +| Tickets | 4 | Deployed | +| Storage | 2 | Deployed | +| Approval Workflows (Enhanced) | 11 | Deployed | +| Courseware (Chapters + Materials + Workbench) | 23 | Deployed | +| Discussion Boards + Posts | 9 | Deployed | +| Announcements | 5 | Deployed | +| Messaging | 6 | Deployed | +| Notifications + Rules + Preferences | 10 | Deployed | +| FAQ (Categories + Items) | 8 | Deployed | +| Plagiarism Detection | 3 | Deployed | +| Official Exam Access | 5 | Deployed | +| Student Leave Management | 7 | **Deployed (Beyond SRS)** | +| Fees Management | 4 | **Deployed (Beyond SRS)** | +| Lessons Management | 4 | **Deployed (Beyond SRS)** | +| Gradebook & Grading | 6 | **Deployed (Beyond SRS)** | +| Student Progress | 1 | **Deployed (Beyond SRS)** | +| Library Management | 9 | **Deployed (Beyond SRS)** | +| Activity Management | 7 | **Deployed (Beyond SRS)** | +| Facility & Asset Management | 7 | **Deployed (Beyond SRS)** | + +--- + +## Appendix B: Model Relationship Diagram + +``` +res.users ──extends──> encoach.user + │ │ + │ ├── entity_ids ──> encoach.entity + │ │ └── role_ids ──> encoach.role + │ │ └── permission_ids ──> encoach.permission + │ │ + │ ├── op_student_id ──> op.student ──> op.student.course ──> op.course + │ │ ├── op.batch + │ │ ├── op.subject + │ │ └── op.department + │ │ + │ └── op_faculty_id ──> op.faculty + │ + ├── encoach.exam ──> encoach.subject (taxonomy) + │ ├── encoach.domain + │ │ └── encoach.topic + │ │ ├── encoach.learning.objective + │ │ ├── encoach.proficiency + │ │ └── encoach.resource + │ │ + │ └── encoach.learning.plan + │ └── encoach.learning.plan.item ──> encoach.topic + │ + ├── op.academic.year ──> op.academic.term + │ + ├── op.admission.register ──> op.admission ──> op.student + │ + ├── op.exam.session ──> op.exam ──> op.exam.attendees + │ └── op.marksheet.register ──> op.marksheet.line + │ + ├── op.assignment ──> op.assignment.sub.line (submissions) + │ + ├── encoach.course.chapter ──> op.course + │ ├── encoach.chapter.material + │ ├── encoach.chapter.progress ──> res.users + │ └── encoach.topic (bridge to adaptive) + │ + ├── encoach.discussion.board ──> op.course / op.batch / encoach.course.chapter + │ └── encoach.discussion.post (self-referencing for threads) + │ + ├── encoach.announcement ──> op.course / op.batch + │ + ├── encoach.message ──> res.users (sender/recipient) + │ + ├── encoach.notification ──> res.users + │ └── encoach.notification.rule (event triggers) + │ + ├── encoach.faq.category ──> encoach.faq.item + │ + ├── encoach.approval.workflow ──> encoach.approval.stage + │ └── encoach.approval.request + │ + └── encoach.plagiarism.report ──> submission +``` + +--- + +*This SRS is the definitive backend specification for the EnCoach platform on Odoo 19. All ~377 REST API endpoints, 41 Odoo modules (14 OpenEduCat + 27 custom), and all AI service integrations have been implemented and deployed to staging at `http://5.189.151.117:8069`. All previous backend SRS documents (`ODOO_BACKEND_SRS_v3.md`, `ODOO_MIGRATION_SRS_v2.md`, `ODOO_MIGRATION_SRS.md`) are superseded by this document.* diff --git a/docs/ENCOACH_PRODUCT_DESCRIPTION.md b/docs/ENCOACH_PRODUCT_DESCRIPTION.md new file mode 100644 index 00000000..805912a1 --- /dev/null +++ b/docs/ENCOACH_PRODUCT_DESCRIPTION.md @@ -0,0 +1,186 @@ +# EnCoach -- Product Description + +**Prepared for:** UTAS University Management +**Date:** March 24, 2026 +**Version:** 1.0 +**Classification:** Client-Facing + +--- + +## 1. Executive Summary + +EnCoach is an AI-powered adaptive learning platform designed to deliver personalized education across multiple academic subjects. The platform automatically assesses each student's current knowledge level, generates a tailored learning plan, delivers the right content at the right time, and continuously adjusts as the student progresses. EnCoach serves two distinct audiences -- **UTAS-enrolled students** who follow university curriculum with AI-enhanced guidance, and **freelance learners** worldwide who access fully AI-driven self-paced programs. The platform launches with three subjects -- **IELTS/English, Mathematics, and Information Technology** -- and is designed to accommodate any additional subject without rebuilding the system. + +--- + +## 2. Product Vision + +### The Problem + +Traditional learning platforms deliver the same material to every student regardless of their existing knowledge. Students waste time reviewing topics they already understand, while critical gaps in their foundation go unaddressed. Teachers lack visibility into individual student needs at scale, and institutions struggle to provide personalized learning outside the classroom. + +### The Solution + +EnCoach combines a proven Learning Management System with an intelligent adaptive engine that treats every student as an individual. The platform: + +- **Discovers what each student knows** through an AI-driven diagnostic assessment +- **Builds a personalized roadmap** that prioritizes weak areas while respecting how topics build on each other +- **Delivers the right content** -- human-curated materials when available, AI-generated explanations when not +- **Adapts in real time** -- as students learn faster or struggle, the plan adjusts automatically +- **Provides an AI coaching companion** available on every page for hints, explanations, and study advice + +### Strategic Value for UTAS + +| Value | Description | +|-------|-------------| +| **Scalable personalization** | Every UTAS student receives an individualized learning experience without increasing teaching staff | +| **Multi-subject from day one** | English, Math, and IT share a single platform; new subjects can be added by defining a topic structure | +| **Revenue expansion** | Freelance students outside UTAS generate subscription revenue on the same platform | +| **Data-driven insights** | Real-time analytics show administrators exactly where students succeed or struggle, per topic, per class, per department | +| **Quality assurance** | AI-generated content is supplementary -- academic staff control the primary learning materials and approve AI outputs | + +--- + +## 3. Key Capabilities + +### 3.1 Adaptive Learning Engine + +The core innovation of EnCoach. When a student begins a new subject, the platform runs an intelligent diagnostic that adapts in real time -- asking harder questions when answers are correct and easier ones when they are not. Within minutes, the system maps the student's strengths and weaknesses across every topic in the subject. + +From this assessment, the AI generates a **personalized learning plan**: a sequenced path through topics the student needs to study, ordered so that foundational concepts are learned before advanced ones. As the student works through the plan, short mastery quizzes confirm understanding before moving on. If a student struggles, the system provides additional resources and intensifies AI coaching. If they progress quickly, the plan accelerates. + +**For UTAS students**, the learning plan aligns with the university's curriculum and semester schedule. Teachers can view and override individual plans. **For freelance students**, the AI generates the complete plan independently based on the student's goals and timeline. + +### 3.2 Multi-Subject Support + +EnCoach is not a single-purpose English testing tool -- it is a universal education platform. Every subject is organized into the same clear hierarchy: + +- **Subject** (e.g., Mathematics) + - **Domain** (e.g., Algebra) + - **Topic** (e.g., Linear Equations) + +This structure supports any academic discipline. The platform launches with: + +| Subject | Coverage | +|---------|----------| +| **IELTS / English** | Reading, Listening, Writing, Speaking, Grammar, Vocabulary, Pronunciation | +| **Mathematics** | Arithmetic, Algebra, Geometry, Statistics & Probability, Calculus | +| **Information Technology** | Computer Fundamentals, Networking, Databases, Programming, Cybersecurity | + +Adding a new subject (e.g., Arabic, Physics, Business) requires only defining its topic structure -- the adaptive engine, exam system, grading, and analytics apply automatically. + +### 3.3 Learning Management System + +A full-featured LMS for structured academic delivery: + +- **Course management** -- Create courses with modules and lessons, assign instructors, set enrollment limits +- **Student enrollment** -- UTAS students enrolled automatically via SIS sync; freelance students self-enroll +- **Batches** -- Group students for cohort-based learning and shared schedules +- **Timetable** -- Schedule and display class sessions for students, teachers, and administrators +- **Attendance** -- Teachers record attendance; students view their own records; administrators monitor patterns +- **Gradebook** -- Centralized grade management with AI-generated narrative reports for academic reviews + +### 3.4 Exam Engine + +A powerful assessment system that goes far beyond multiple-choice: + +- **AI-generated exams** -- The platform creates new exam content on demand for any subject and difficulty level. English exams include reading passages, listening scripts, writing tasks, and speaking prompts. Math exams include numerical problems, word problems, and multi-step worked problems. IT exams include code completion, scenario-based questions, and true/false items. +- **AI-powered grading** -- Writing and speaking responses are graded automatically against detailed rubrics. Math answers are checked with numerical tolerance. IT code answers are evaluated for correctness and approach. All AI grades can be reviewed and approved by teachers through an approval workflow. +- **AI writing detection** -- Every written submission is scanned for AI-generated content, providing per-sentence confidence scores. +- **Multiple delivery modes** -- Practice exams, timed official exams, scheduled assignments, diagnostic assessments, and mastery quizzes. + +### 3.5 AI Assistant + +An intelligent companion embedded throughout the platform: + +| What It Does | Where | +|-------------|-------| +| **Answers questions** in natural language about any topic | Available on every page via a chat drawer | +| **Gives hints** during practice exercises without revealing the answer | Practice and study pages | +| **Explains grades** so students understand what to improve | After exam results | +| **Suggests what to study next** based on progress and weaknesses | Student dashboard | +| **Generates study tips** personalized to the student's current topic | Topic learning pages | +| **Helps teachers create content** -- course outlines, assignments, rubrics | Teacher course builder | +| **Produces report narratives** for academic reviews | Admin reports | +| **Flags at-risk students** based on attendance, grades, and engagement patterns | Teacher and admin dashboards | + +### 3.6 Administration and Reporting + +Comprehensive tools for managing the platform at every level: + +- **User management** -- Create, search, filter, and export student, teacher, and corporate accounts +- **Multi-tenant organizations** -- Each institution or company operates in its own space with its own roles, permissions, and branding +- **Classroom management** -- Create student groups, transfer students between classes +- **Assignment scheduling** -- Schedule exams with start/end dates, assign to classrooms or individual students, track completion +- **Permission system** -- Granular control over who can view, create, edit, or delete content, organized by subject +- **Analytics dashboards** -- Real-time metrics on student performance, subject completion rates, AI usage, content gaps, and resource utilization +- **Support ticketing** -- Built-in helpdesk for students and staff to report issues +- **Payment management** -- Institutional licensing for UTAS, individual subscriptions for freelance students, with support for multiple payment providers + +--- + +## 4. User Experience + +### The Student Experience + +A student logs in and sees their personalized dashboard: current subjects, learning plan progress, upcoming assignments, and a study streak counter. They select a subject and see their mastery profile -- a visual map showing which topics they have mastered, which they are working on, and which are locked until prerequisites are completed. Clicking a topic opens the learning page with curated materials (PDFs, videos, articles) and AI-generated explanations. After studying, a short mastery quiz confirms understanding. Throughout the experience, the AI coach is always available for hints, explanations, and encouragement. The student can also attend scheduled classes, view their timetable, check grades, and track attendance -- all from a single, unified interface. + +### The Teacher Experience + +A teacher logs in to see their class dashboard with insights at a glance: which students are progressing, which are at risk, and where content gaps exist. They can create courses using the AI course builder that generates outlines from a topic description. When grading, the AI Grading Assistant suggests marks and provides feedback drafts that the teacher can accept, modify, or override. Teachers can view any student's learning plan, adjust it if needed, and monitor mastery across their entire class through a heatmap visualization. Attendance recording and timetable management are integrated directly into their workflow. + +### The Administrator Experience + +An administrator has full visibility across the entire platform. The LMS dashboard shows enrollment numbers, completion rates, and subject-level performance across all courses. The platform dashboard provides user counts, entity statistics, and system health. Administrators manage organizations, roles, and permissions; they create exam structures, configure approval workflows, and oversee the content library. AI-powered reports generate narrative summaries for academic reviews, and the batch optimizer suggests class compositions for balanced learning outcomes. + +--- + +## 5. How AI Powers the Platform + +EnCoach integrates six specialized AI capabilities, each designed for a specific educational purpose: + +| Capability | What It Does for Users | +|-----------|----------------------| +| **Intelligent Conversation** | Powers the AI Assistant, the study coach, grade explanations, writing help, and content generation. Understands context -- it knows what page you are on, what topic you are studying, and what your recent performance looks like. | +| **Speech Recognition** | Converts student voice recordings into text for speaking exam grading. Supports multiple accents and speaking styles. | +| **Text-to-Speech** | Generates natural-sounding audio for English listening exams. Supports 11 distinct voices with regional accents for authentic test preparation. | +| **AI Video Avatars** | Creates video-based speaking prompts with realistic virtual presenters, making speaking practice more engaging and exam-like. | +| **Content Authenticity** | Scans written submissions to detect AI-generated text, ensuring academic integrity with per-sentence confidence analysis. | +| **Smart Recommendations** | Finds the most relevant training tips, study materials, and content recommendations based on semantic understanding of each student's needs -- not just keyword matching. | + +All AI-generated content is supplementary. Academic staff control primary learning materials, and AI outputs can be routed through approval workflows before students see them. + +--- + +## 6. Integration with UTAS + +| Integration Area | How It Works | +|-----------------|-------------| +| **Student Information System (SIS)** | Student enrollment data syncs automatically from UTAS SIS. New students appear in EnCoach without manual data entry. Student records stay synchronized throughout the semester. | +| **Curriculum Alignment** | Learning plans for UTAS students follow the university's curriculum. The topic taxonomy maps to UTAS course structures, ensuring adaptive learning enhances rather than replaces the academic program. | +| **Institutional Licensing** | UTAS operates under an institutional license covering all enrolled students and staff. No individual subscriptions needed for university members. | +| **Teacher and Admin Oversight** | UTAS teachers see their students' AI-generated learning plans and can adjust them. Administrators have full visibility into performance analytics across all departments and subjects. | +| **Branding** | The platform can display UTAS branding (logo, colors) for an institution-native experience. | + +--- + +## 7. Platform at a Glance + +| Dimension | Details | +|-----------|---------| +| **Subjects** | IELTS/English, Mathematics, Information Technology (extensible to any subject) | +| **User Roles** | Student, Teacher, Admin, Corporate, Master Corporate, Agent, Developer | +| **Student Modes** | UTAS Institutional (SIS-synced, curriculum-aligned) and Freelance (self-registered, AI-guided) | +| **AI Capabilities** | Adaptive diagnostics, personalized learning plans, content generation, automated grading, real-time coaching, writing integrity detection | +| **LMS Features** | Courses, batches, timetable, attendance, gradebook, reporting | +| **Exam Types** | Practice, official, assignment-based, diagnostic, mastery quiz | +| **Content Model** | Hybrid -- human-uploaded resources prioritized, AI-generated content fills gaps | +| **Payment Options** | Institutional license, individual subscription, bundled packages | +| **Payment Providers** | Stripe, PayPal, Paymob | +| **Security** | Server-side authorization, role-based access control, encrypted authentication, data isolation between organizations | +| **Deployment** | Cloud-hosted, containerized architecture, automated scaling | +| **Accessibility** | Responsive design (desktop and mobile), accessible form controls, screen reader support | + +--- + +*This document describes the EnCoach platform as specified in the Unified Platform SRS (v1.0, March 2026). For detailed technical specifications, refer to ENCOACH_UNIFIED_SRS.md.* diff --git a/docs/ENCOACH_SYSTEM_FEATURES_GUIDE.md b/docs/ENCOACH_SYSTEM_FEATURES_GUIDE.md new file mode 100644 index 00000000..6ebba0b4 --- /dev/null +++ b/docs/ENCOACH_SYSTEM_FEATURES_GUIDE.md @@ -0,0 +1,308 @@ +# EnCoach Platform -- System Features Guide + +**Version:** 1.0 +**Date:** March 2026 +**Purpose:** Describe every section and feature accessible through the platform navigation, organized by user role. + +--- + +## Part 1: Administrator Panel + +The Administrator Panel is the central management hub for the entire platform. It is accessible to users with roles: Admin, Corporate, Master Corporate, Agent, and Developer. + +--- + +### 1. Overview + +The Overview section provides high-level dashboards that give administrators an at-a-glance view of the entire platform's health and activity. + +| Menu Item | Description | +|-----------|-------------| +| **Admin Dashboard** | The LMS-focused dashboard showing real-time counts of courses, students, teachers, batches, and enrollment status. Displays course capacity charts, student status distribution, and recent activity. | +| **Platform Dashboard** | The original platform dashboard focused on exam and assignment activity. Shows exam creation statistics, assignment completion rates, user activity trends, and entity-level performance summaries. | + +--- + +### 2. LMS + +The LMS (Learning Management System) section is the core of institutional course management. It handles the full lifecycle of courses, students, teachers, and class scheduling. + +| Menu Item | Description | +|-----------|-------------| +| **Courses** | Create, view, and manage courses. Each course has a title, code, description, maximum capacity, status (draft/active/archived), and is linked to a department. Administrators can view enrollment counts and manage course settings. | +| **Students** | Manage student records. Create new students (which automatically creates a portal user account), view student lists with filters, export student data, and manage individual student profiles including their course enrollments. | +| **Teachers** | Manage teacher records. Create teacher profiles with specialization areas, link them to portal user accounts, assign them to courses and batches, and view their teaching schedules. | +| **Batches** | Manage class batches (sections/cohorts). Each batch belongs to a course and has a date range, capacity, and enrolled students. Administrators can view batch details, manage student enrollment per batch, and track batch capacity. | +| **Lessons** | Create and manage individual lessons linked to a course, batch, and subject. Each lesson has a faculty member, date, and duration. Used to organize the day-to-day teaching schedule within a course. | +| **Timetable** | Visual timetable management with a day/time grid. Create, view, and delete teaching sessions. Sessions are linked to courses, batches, subjects, and classrooms with specific day and time slots. | +| **Reports** | LMS-specific reporting dashboards showing course enrollment trends, batch capacity utilization, student status distributions, teacher workload, and overall LMS usage analytics. | + +--- + +### 3. Adaptive Learning + +The Adaptive Learning section manages the AI-powered personalized learning engine. It controls the knowledge structure that drives diagnostic tests, proficiency tracking, and personalized learning plans. + +| Menu Item | Description | +|-----------|-------------| +| **Taxonomy** | Manage the subject knowledge hierarchy: Subjects (e.g., English, Math, IT) contain Domains, which contain Topics, which contain Learning Objectives. This taxonomy drives the adaptive engine -- diagnostic tests assess proficiency at the topic level, and learning plans are generated based on this structure. Administrators can create subjects, add domains/topics manually, or use AI to suggest topic structures. | +| **Resources** | Manage the learning resource library. Upload educational materials (PDFs, videos, links, documents, interactive content) and tag them to specific topics in the taxonomy. Resources go through a review workflow (pending, approved, rejected) and are served to students through their personalized learning plans. Track resource downloads, completion, and ratings. | + +--- + +### 4. Institutional + +The Institutional section handles university-level academic administration -- the calendar, departments, enrollment pipeline, formal exam sessions, grading, and student lifecycle. + +| Menu Item | Description | +|-----------|-------------| +| **Academic Years** | Define academic years with start and end dates. Auto-generate academic terms (semesters/quarters) within each year. Academic years organize all institutional activities -- courses, batches, exams, and grades are all scoped to an academic year. | +| **Departments** | Manage the department hierarchy with parent-child relationships. Departments organize courses and faculty. Each department can have sub-departments, a head of department, and associated courses. | +| **Admissions** | View and process individual student admission applications. Each admission goes through a workflow: submitted, confirmed, admitted, or rejected. Administrators can review application details, update status, and convert admitted applicants into enrolled students. | +| **Admission Register** | Manage admission campaigns/registers. Each register defines an admission period (open/closed), linked course, available seats, and start/end dates. Registers go through a workflow: create, confirm (open for applications), close (stop accepting). | +| **Exam Sessions** | Schedule and manage institutional exam sessions. Each session is linked to a course, batch, and academic term. Sessions go through a workflow: draft, scheduled, in progress, done. Manage exam rooms, attendees, and timing within each session. | +| **Marksheets** | Generate and validate student marksheets (grade reports). Marksheets aggregate student scores across exams within a session, apply grade configurations and result templates, and compute pass/fail status. Supports validation workflow before official release. | +| **Student Leave** | Manage student leave requests. Students submit leave requests with reasons and dates; administrators or teachers can approve or reject them. Tracks leave history per student. | +| **Fees** | View fee plans, fee terms, and individual student fee records. Manage fee structures attached to courses, including installment plans and payment tracking. | +| **Gradebook** | View and manage the institutional grading system. Displays grades per student, per course, and per academic year. Supports grading assignment types, weighted grading, and grade configuration (letter grades, percentage scales). | +| **Student Progress** | Track longitudinal student progress across courses, subjects, and academic terms. Provides a historical view of student performance, showing trends and areas that need attention. | + +--- + +### 5. Academic + +The Academic section manages the AI-powered exam engine -- creating exams, defining grading rubrics, generating content with AI, and managing the approval workflow for publishing exams. + +| Menu Item | Description | +|-----------|-------------| +| **Assignments** | Create and manage exam-linked assignments. Each assignment wraps an exam and assigns it to specific students or classrooms with deadlines. Supports tabs for Active, Planned, Past, and Archived assignments. Includes late submission policies, extension requests, penalty configuration, and reminder settings. | +| **Exams List** | Browse, search, and manage all exams in the system. Filter by subject, module (Reading, Writing, Listening, Speaking), and difficulty. Each exam contains exercises, passages, timer settings, and access controls (public/private). Supports both practice exercises and official exams. | +| **Exam Structures** | Define exam blueprints (templates) that specify the section layout of exams -- how many sections, what type of questions in each, difficulty distribution, and time allocation per section. Structures are reusable across multiple exams. | +| **Rubrics** | Create and manage grading rubrics that define the criteria for evaluating student responses. Rubrics specify scoring dimensions (e.g., Task Achievement, Coherence, Grammar for IELTS writing). Rubric groups organize related rubrics. Used by both human graders and the AI grading engine. | +| **Generation** | The AI exam generation interface. Select a subject, module, and topic; configure difficulty and question types; and let the AI (GPT-4o) generate complete exams with passages, questions, and answer keys. Supports generation from scratch or from existing content. | +| **Approval Workflows** | Manage the approval process for publishing exams and course materials. Configure multi-stage workflows with specific approvers, time limits per stage, auto-escalation rules, and bypass options. Track approval status (pending, approved, rejected, escalated) with comments through each stage. | + +--- + +### 6. Management + +The Management section handles platform-wide user, organization, and infrastructure management. + +| Menu Item | Description | +|-----------|-------------| +| **Users** | Full user management. Create, update, search, filter, and export users. View user details including role, entity, verification status, and activity history. Supports batch user import from spreadsheets. | +| **Entities** | Manage organizations (multi-tenant). Each entity is an institution or company using the platform. Configure entity settings, manage entity-specific roles and permissions, generate invite codes for onboarding, and set per-entity branding (logo, colors). | +| **Classrooms** | Manage virtual and physical classroom groups. Create classrooms with capacity tracking, assign students to classrooms, and use classrooms for assigning exams and assignments to groups of students. | +| **Roles & Permissions** | Create and manage roles within entities. Each role has a name and a set of granular permissions. Administrators can create custom roles, assign/remove permissions from roles, and manage the permission catalog. | +| **Authority Matrix** | A cross-reference visualization of all roles and their permissions. Displayed as an interactive grid where rows are roles and columns are permissions. Administrators can toggle permissions on/off directly from the matrix view. | +| **User Roles** | Assign roles to individual users. View all users with their current role assignments, search and filter, and assign or remove roles. Supports user-level permission overrides beyond their assigned role. | + +--- + +### 7. Reports + +The Reports section provides analytical views of platform performance and student outcomes. + +| Menu Item | Description | +|-----------|-------------| +| **Student Performance** | Detailed performance analytics per student. Shows exam scores, assignment completion rates, proficiency levels across subjects, and trends over time. Supports filtering by entity, course, and date range. | +| **Stats Corporate** | Entity-level statistics for corporate/institutional managers. Shows aggregated data across all students within an entity -- enrollment counts, average scores, completion rates, and comparative performance across courses. | +| **Record** | Historical activity records. Browse past exam sessions, assignment submissions, and user activity logs. Supports searching and filtering by date, user, and activity type. | + +--- + +### 8. Configuration + +The Configuration section manages platform-wide settings for notifications, FAQs, and approval processes. + +| Menu Item | Description | +|-----------|-------------| +| **FAQ Manager** | Create and manage Frequently Asked Questions. Organize FAQs into categories, set target audience per item (student, entity, or both), add rich content including text, images, and video links. FAQs are displayed on a public searchable page with accordion-style expand/collapse. | +| **Notification Rules** | Configure automated notification triggers. Define rules based on event types (assignment due, exam due, chapter unlock, result release), set timing (how many days before the event), frequency (once, daily, custom intervals), and delivery channel (in-app, email, or both). | +| **Approval Config** | Configure the enhanced approval workflow system. Define multi-stage approval templates with specific approvers per stage, maximum days before auto-escalation, notification emails, and bypass permissions. Applied to exam publishing and course material approval. | + +--- + +### 9. Training + +The Training section provides reference materials for language learning support. + +| Menu Item | Description | +|-----------|-------------| +| **Vocabulary** | Browse vocabulary training content. Displays categorized vocabulary lists, definitions, usage examples, and related exercises. Linked to the FAISS-based recommendation engine for contextual suggestions. | +| **Grammar** | Browse grammar training content. Displays grammar rules, examples, common mistakes, and practice exercises organized by topic and difficulty level. | + +--- + +### 10. Support + +The Support section handles financial tracking, user support, and system settings. + +| Menu Item | Description | +|-----------|-------------| +| **Payment Record** | View payment history and transaction records. Shows all subscription payments, amounts, providers (Stripe/PayPal/Paymob), status, and associated packages. Supports filtering by date, user, and payment status. | +| **Tickets** | Manage support tickets. View all tickets submitted by users, assign tickets to support agents, update ticket status (open, in progress, resolved, closed), and communicate with ticket reporters. | +| **Settings** | Platform-wide settings management. Configure system parameters, AI service settings (API keys stored securely), default behaviors, and platform-level preferences. | + +--- + +### 11. Other Admin Pages (Accessible but not in main sidebar) + +| Page | Description | +|------|-------------| +| **Library** | Manage the institutional library. CRUD operations for library media (books, journals, digital resources), track media movements (issue, return), and manage student library cards. | +| **Activities** | Log and track student activities. Define activity types and record student participation in extracurricular or academic activities outside the standard course structure. | +| **Facilities** | Manage institutional facilities and assets. Track rooms, equipment, and other physical resources used for teaching and examinations. | + +--- + +## Part 2: Teacher Panel + +The Teacher Panel provides instructors with tools to manage their courses, create content, track student progress, and communicate with their classes. + +--- + +### 1. Teaching + +The Teaching section is the teacher's primary workspace for delivering courses and managing assignments. + +| Menu Item | Description | +|-----------|-------------| +| **Dashboard** | The teacher's home screen showing an overview of their assigned courses, upcoming assignments, recent student submissions, and quick statistics (number of students, pending grading, upcoming sessions). | +| **Courses** | View and manage assigned courses. From here, teachers can access three key sub-pages per course: **Chapters** (organize course content into sequential chapters with materials, set unlock dates, and control access), **Chapter Detail** (upload and manage materials within a chapter -- PDFs, videos, audio, images, links -- with download permission controls), and **AI Workbench** (generate course content using AI by inputting topic, objectives, and complexity level; review and publish AI-generated chapters, materials, exercises, and rubrics). | +| **Assignments** | Create and manage assignments for classes. Define deadlines, late submission policies, penalty structures, and reminder frequencies. View assignment submissions, check plagiarism reports, grade with AI assistance, and release results. Drill into individual assignments to see per-student submission status and grading. | + +--- + +### 2. Management + +The Management section gives teachers tools to track student attendance and schedules. + +| Menu Item | Description | +|-----------|-------------| +| **Attendance** | Take and manage attendance for classes. Mark students as present, absent, or late for each session. View attendance history and trends per batch. | +| **Students** | View the list of students enrolled in the teacher's courses and batches. Access individual student profiles and performance summaries. | +| **Timetable** | View the teacher's personal teaching schedule in a visual day/time grid format. Shows all assigned sessions across courses and batches. | + +--- + +### 3. Communication + +The Communication section enables teacher-student interaction and class announcements. + +| Menu Item | Description | +|-----------|-------------| +| **Discussions** | View and manage discussion boards for courses. Teachers can enable/disable discussion boards per course or chapter, create discussion threads, reply to student questions, pin important posts, and mark questions as resolved. Supports file attachments. | +| **Announcements** | Create and broadcast announcements to classes. Set priority (normal, important, urgent), target specific courses or batches, optionally send via email in addition to in-app notification. Manage drafts and publish when ready. | + +--- + +### 4. Account + +| Menu Item | Description | +|-----------|-------------| +| **Profile** | View and update personal profile information (name, email, specialization, contact details). | + +--- + +## Part 3: Student Panel + +The Student Panel provides learners with access to their courses, adaptive learning engine, progress tracking, and communication tools. + +--- + +### 1. Learning + +The Learning section is the student's primary workspace for accessing courses and completing assignments. + +| Menu Item | Description | +|-----------|-------------| +| **Dashboard** | The student's home screen showing enrolled subjects, upcoming deadlines (assignments, quizzes, exams), recent announcements, personal progress statistics, and AI study tips. Includes an AI coaching assistant for on-demand help. | +| **My Courses** | Browse all enrolled courses. Each course shows its chapters, completion progress, and available materials. Students can navigate into individual courses to see the chapter structure, access unlocked chapter content, download materials (when permitted by the teacher), and track their progress through the course. | +| **Subject Registration** | Register for available subjects within the current academic term. View available subjects, prerequisites, and capacity. Submit registration requests for subjects the student wants to take. | +| **Assignments** | View all active, upcoming, and past assignments. Submit completed work through the platform, view deadlines and submission status, request deadline extensions (when allowed), track submission history, and receive AI-generated feedback and plagiarism reports. | + +--- + +### 2. Adaptive Learning + +The Adaptive Learning section provides AI-powered personalized education that adapts to each student's level. + +| Menu Item | Description | +|-----------|-------------| +| **My Subjects** | The entry point for the adaptive learning engine. Displays all subjects the student is studying with their overall mastery level. From here, students can: take a **Diagnostic Test** (AI-administered assessment to determine initial proficiency across topics), view their **Proficiency Profile** (detailed breakdown of mastery per topic within a subject), access their **Learning Plan** (AI-generated personalized study path with ordered topics based on proficiency gaps), and study individual **Topics** (AI-curated learning content with practice exercises and mastery quizzes that adapt difficulty based on performance). | + +--- + +### 3. Progress + +The Progress section shows academic performance, attendance, and the overall learning journey. + +| Menu Item | Description | +|-----------|-------------| +| **Grades** | View grades for all courses, assignments, and exams. Shows current marks, feedback from teachers, and detailed score breakdowns by grading criteria. | +| **Attendance** | View personal attendance records across all enrolled courses. Shows attendance percentage, absent dates, and any leave requests with their approval status. | +| **Timetable** | View personal class schedule in a visual day/time grid. Shows all upcoming sessions with course, subject, teacher, and room information. | +| **My Journey** | A longitudinal progress timeline showing the student's overall academic journey. Displays enrolled courses with completion percentages, per-subject proficiency trends, and a recent activity timeline (exams taken, assignments submitted, chapters completed). Provides a holistic view of growth over time. | + +--- + +### 4. Communication + +The Communication section enables students to interact with teachers and classmates. + +| Menu Item | Description | +|-----------|-------------| +| **Discussions** | Participate in course and chapter discussion boards. Create new discussion threads, reply to existing threads (nested replies), ask questions about assignments or course content, and view pinned/resolved posts. Students can see other participants within the same class. | +| **Messages** | Direct messaging between users. View inbox (with read/unread indicators and unread count badge), send messages to teachers or classmates within the same class, attach files, and optionally send an email copy. Includes a sent messages view. | +| **Announcements** | View announcements from teachers and system administrators. Announcements are displayed with priority indicators (urgent, important, normal) and sorted by date. | + +--- + +### 5. Account + +| Menu Item | Description | +|-----------|-------------| +| **Profile** | View and update personal profile information (name, email, contact details, preferences). | + +--- + +## Part 4: Public Pages (No Login Required) + +These pages are accessible without authentication. + +| Page | Description | +|------|-------------| +| **Login** | User authentication with email and password. Redirects to the appropriate dashboard based on user role after successful login. | +| **Register** | New user registration. Supports open registration or invite-code-based registration to join a specific entity with a predefined role. | +| **Forgot Password** | Password recovery via email. Users enter their email to receive a verification link for password reset. | +| **Admission Application** | Public admission form for prospective students. Users can apply to an open admission register without logging in, providing personal details and required documents. They can check application status afterward. | +| **FAQ** | Public Frequently Asked Questions page. Searchable, accordion-style display of FAQ items organized by category. Content is filtered based on the user's role (student or entity) if logged in, showing all items if accessed publicly. | +| **Official Exam Access** | Special exam access pages for official/proctored exams. Supports three modes: (1) token-based link access where students open a unique URL, (2) landing page access with an exam code, and (3) a dedicated exam login screen isolated from the rest of the platform. | + +--- + +## Part 5: AI Features (Embedded Across the Platform) + +AI capabilities are embedded throughout the platform rather than being a standalone section. Here is where AI appears: + +| Feature | Where It Appears | What It Does | +|---------|-----------------|-------------| +| **AI Study Coach** | Student Dashboard | Chat-based coaching assistant that provides hints, concept explanations, writing help, and study tips. | +| **AI Exam Generation** | Admin > Generation, Teacher > AI Workbench | Generates complete exams (passages, questions, answer keys) from subject taxonomy using GPT-4o. | +| **AI Grading** | Admin > Assignments, Teacher > Assignments | Automatically grades writing (rubric-based IELTS band scoring), speaking (transcription + evaluation), math (step evaluation), and IT (code evaluation) using GPT-4o. | +| **AI Course Content** | Teacher > AI Workbench | Generates course outlines, chapter content, exercises, and grading rubrics from high-level requirements. | +| **AI Diagnostics** | Student > My Subjects | Administers adaptive diagnostic tests that adjust question difficulty based on responses to accurately assess proficiency. | +| **AI Learning Plans** | Student > My Subjects | Generates personalized learning plans based on diagnostic results and proficiency gaps. | +| **AI Content Recommendations** | Student > Topic Learning | Suggests additional learning materials based on student performance and learning history using FAISS similarity search. | +| **AI Plagiarism Detection** | Teacher > Assignments, Admin > Assignments | Analyzes submitted text using GPTZero to detect AI-generated content, providing per-sentence analysis and downloadable reports. | +| **AI Search** | Admin Header Bar | Semantic search across the platform using AI. | +| **AI Analytics** | Admin > Reports | Generates narrative reports, identifies content gaps, and provides batch optimization suggestions. | +| **AI Tip Banner** | Student Dashboard | Context-aware training tips selected using FAISS-based recommendation. | +| **Text-to-Speech** | Listening Exams | Converts exam text to natural speech using AWS Polly (11 neural voices). | +| **Speech-to-Text** | Speaking Exams | Transcribes student speaking responses using local Whisper model. | +| **AI Avatar Videos** | Speaking Exams | Generates avatar-based video prompts for speaking exams using ELAI. | + +--- + +*End of System Features Guide* diff --git a/docs/ENCOACH_UNIFIED_SRS.md b/docs/ENCOACH_UNIFIED_SRS.md new file mode 100644 index 00000000..c6458496 --- /dev/null +++ b/docs/ENCOACH_UNIFIED_SRS.md @@ -0,0 +1,3223 @@ +# EnCoach Unified Platform -- Software Requirements Specification + +**Document Version:** 2.0 +**Date:** March 11, 2026 +**Status:** Implemented -- Staging Verified +**Supersedes:** `ielts-ui/SRS-EnCoach.md` (v2.0), `MATH_IT_ADAPTIVE_LEARNING_SRS.md` (v1.0), `ENCOACH_UNIFIED_SRS.md` (v1.0) +**Author:** EnCoach Engineering Team + +### Implementation Status + +| Artifact | Location | +|----------|----------| +| **Frontend Repository** | `https://git.albousalh.com/devops/encoach_frontend_new_v2.git` (branch: `main`) | +| **Backend Repository** | `https://git.albousalh.com/devops/encoach_backend_new_v2.git` (branch: `main`) | +| **Staging Frontend** | `http://5.189.151.117:3000` | +| **Staging Backend (Odoo 19)** | `http://5.189.151.117:8069` | +| **Frontend Stack** | React 18, TypeScript, Vite 5, shadcn/ui, TanStack Query, React Router v6 | +| **Backend Stack** | Odoo 19, Python 3.11, PostgreSQL 16, Docker Compose | +| **Frontend Pages** | 93 page components (28 root, 33 admin, 19 student, 14 teacher) | +| **Frontend Services** | 37 API service modules | +| **Frontend Types** | 29 TypeScript type definition files | +| **Frontend Query Hooks** | 21 TanStack Query hook modules | +| **Backend Custom Modules** | 27 `encoach_*` modules | +| **Backend OpenEduCat Modules** | 14 `openeducat_*` community modules | +| **Backend API Routes** | ~377 REST endpoints across 4 controller packages | + +All sections in this SRS have been implemented in both frontend and backend unless otherwise noted. The developer implemented all original SRS requirements plus additional OpenEduCat Enterprise-level features documented in Part VIII-B. + +--- + +## Table of Contents + +**Part I -- Platform Foundation** +1. [Introduction](#1-introduction) +2. [System Architecture](#2-system-architecture) +3. [User Roles and Multi-Tenancy](#3-user-roles-and-multi-tenancy) +4. [Authentication and Authorization](#4-authentication-and-authorization) +5. [Global UI Shell](#5-global-ui-shell) + +**Part II -- Universal Subject Engine** +6. [Subject Taxonomy](#6-subject-taxonomy) +7. [Diagnostic Assessment](#7-diagnostic-assessment) +8. [Proficiency Profile](#8-proficiency-profile) +9. [Learning Plan Generation](#9-learning-plan-generation) +10. [Hybrid Content Delivery](#10-hybrid-content-delivery) +11. [Progress Tracking and Mastery](#11-progress-tracking-and-mastery) + +**Part III -- LMS Module** +12. [Course Management](#12-course-management) +13. [Batch and Enrollment](#13-batch-and-enrollment) +14. [Timetable and Attendance](#14-timetable-and-attendance) +15. [Gradebook and Reports](#15-gradebook-and-reports) + +**Part IV -- Exam Engine** +16. [Exam Management](#16-exam-management) +17. [AI Content Generation](#17-ai-content-generation) +18. [Exam Delivery](#18-exam-delivery) +19. [AI Grading](#19-ai-grading) + +**Part V -- AI Services** +20. [AI Services Catalog](#20-ai-services-catalog) +21. [AI Integration in Frontend](#21-ai-integration-in-frontend) + +**Part VI -- Administration** +22. [User Management](#22-user-management) +23. [Entity Management](#23-entity-management) +24. [Classroom Management](#24-classroom-management) +25. [Assignment Management](#25-assignment-management) +26. [Permission Management](#26-permission-management) + +**Part VII -- Support and Commercial** +27. [Ticketing System](#27-ticketing-system) +28. [Subscriptions and Payments](#28-subscriptions-and-payments) +29. [Training Content](#29-training-content) + +**Part VIII -- Institutional LMS (OpenEduCat)** +30. [Academic Year and Term Management](#30-academic-year-and-term-management) +31. [Department Management](#31-department-management) +32. [Admission and Enrollment](#32-admission-and-enrollment) +33. [Subject Registration](#33-subject-registration) +34. [Institutional Exam Sessions](#34-institutional-exam-sessions) +35. [Assignment File Submissions](#35-assignment-file-submissions) + +**Part VIII-B -- OpenEduCat Enterprise Features** +36. [Student Leave Management](#36-student-leave-management) +37. [Fees Management](#37-fees-management) +38. [Lessons Management](#38-lessons-management) +39. [Gradebook and Grading Assignments](#39-gradebook-and-grading-assignments) +40. [Student Progress Tracking](#40-student-progress-tracking) +41. [Library Management](#41-library-management) +42. [Activity Management](#42-activity-management) +43. [Facility and Asset Management](#43-facility-and-asset-management) + +**Part IX -- Courseware and Content Delivery** +44. [Course Chapters](#44-course-chapters) +45. [Chapter Materials](#45-chapter-materials) +46. [AI Workbench for Course Content](#46-ai-workbench-for-course-content) + +**Part X -- Communication** +47. [Discussion Boards](#47-discussion-boards) +48. [Announcements](#48-announcements) +49. [Messaging](#49-messaging) + +**Part XI -- Notifications and FAQ** +50. [Notification Engine](#50-notification-engine) +51. [FAQ System](#51-faq-system) + +**Part XII -- Technical Specifications** +52. [Data Models](#52-data-models) +53. [REST API Specification](#53-rest-api-specification) +54. [Frontend Page Inventory](#54-frontend-page-inventory) +55. [Non-Functional Requirements](#55-non-functional-requirements) + +--- + +# Part I -- Platform Foundation + +## 1. Introduction + +### 1.1 Purpose + +This document is the definitive SRS for the EnCoach platform rebuild. It specifies all functional and non-functional requirements for a unified AI-powered education platform that serves: + +- **UTAS students** (institutional, SIS-synced, curriculum-aligned learning paths) +- **Freelance students** (self-registered, AI-generated learning plans, subscription-based) + +The platform supports multiple subjects (IELTS/English, Mathematics, Information Technology, and future subjects) through a universal adaptive learning engine. + +### 1.2 Scope + +**In scope:** +- All production features from the existing EnCoach platform (behavioural parity) +- Universal adaptive learning engine (diagnostic, proficiency, learning plans, mastery) +- Multi-subject support (IELTS, Math, IT -- extensible to any subject) +- LMS module (OpenEduCat-backed courses, batches, timetable, attendance) +- Multi-tenant operation (UTAS institutional + freelance self-service) +- AI services integration (GPT-4o, Whisper, Polly, ELAI, GPTZero, FAISS) +- New React frontend with real AI integration (replacing simulated components) +- Odoo 19 backend with REST API + +**Out of scope:** +- Mobile native applications (iOS/Android) +- Pixel-perfect recreation of old UI (new UI/UX takes precedence) +- Database internals beyond Odoo model specifications +- Hosting SLAs and legal/commercial terms + +### 1.3 Definitions + +| Term | Definition | +|------|-----------| +| **Subject** | Top-level academic discipline (IELTS/English, Mathematics, IT). Subjects are the root of the taxonomy. | +| **Domain** | Major area within a subject (e.g., Algebra, Networking, Writing). | +| **Topic** | Specific teachable unit within a domain (e.g., Linear Equations, TCP/IP). | +| **Learning Objective** | Measurable skill a student should gain from a topic. | +| **Entity** | Tenant/organization with licenses, roles, and scoped permissions. UTAS is an entity. | +| **Classroom** | User group with an admin and participants, scoped to an entity. | +| **Assignment** | Scheduled set of exams for assignees; deep link `/exam?assignment={id}`. | +| **Skill Module** | Legacy term for IELTS skill areas (reading, listening, writing, speaking, level). In the new system, skill modules are topics within the English subject. | +| **Mastery Level** | Numeric score (0-100) indicating proficiency on a topic. | +| **Learning Plan** | AI-generated, sequenced list of topics for a student to study. | +| **Resource** | Human-uploaded learning material (PDF, video, link) tagged to topics. | +| **UTAS Student** | Student enrolled through UTAS SIS sync, following institutional curriculum. | +| **Freelance Student** | Self-registered student with AI-generated learning plans, individual subscription. | + +### 1.4 Key Architectural Decisions + +| # | Decision | Rationale | +|---|----------|-----------| +| AD-1 | **Universal Subject Engine replaces hardcoded IELTS** | The exam module's `MODULE_SELECTION` becomes dynamic, driven by subject taxonomy. "Reading, Listening, Writing, Speaking" become skill types within the English subject, not hardcoded platform concepts. This allows Math, IT, and future subjects to use the same engine. | +| AD-2 | **Two student modes** | UTAS students follow curriculum + adaptive learning with SIS sync. Freelance students get fully AI-generated learning plans with no curriculum dependency. Both share the same platform. | +| AD-3 | **AI components become real** | Each of the 15 simulated AI components in the new frontend maps to specific backend AI services. No simulation in production. | +| AD-4 | **OpenEduCat as LMS backbone** | Course, batch, timetable, and attendance models come from OpenEduCat Odoo modules. A REST API bridge exposes them to the React frontend. | +| AD-5 | **Permission model extended for multi-subject** | Current hardcoded permissions (`generate_reading`, `view_writing`, etc.) are replaced with subject-parameterized permissions (`generate_exam:{subject}`, `view_exam:{subject}`). | + +--- + +## 2. System Architecture + +### 2.1 Architecture Overview + +```mermaid +graph TB + subgraph clients ["Client Layer"] + Browser["React 18 SPA
Vite + TypeScript + shadcn/ui
50+ pages"] + end + + subgraph gateway ["API Gateway"] + Nginx["Nginx
SSL Termination
Route Proxy"] + end + + subgraph backend ["Backend -- Odoo 19"] + API["REST API Controllers"] + Core["Core Modules
Users, Entities, Roles"] + Adaptive["Adaptive Engine
Taxonomy, Diagnostic,
Proficiency, Learning Plans"] + Exam["Exam Engine
Universal multi-subject"] + LMS["OpenEduCat LMS
Courses, Batches,
Timetable, Attendance"] + AImod["AI Modules
Generation, Grading,
Media, Coaching"] + Biz["Business Modules
Subscriptions, Tickets,
Registration, Assignments"] + end + + subgraph ai ["AI Services"] + GPT["OpenAI GPT-4o"] + Whisper["Whisper STT
(local)"] + Polly["AWS Polly TTS"] + ELAI_svc["ELAI Avatars"] + GPTZero_svc["GPTZero"] + FAISS_svc["FAISS + SentenceTransformers"] + end + + subgraph data ["Data"] + PG["PostgreSQL 16"] + end + + subgraph external ["External"] + SIS["UTAS SIS"] + Stripe_svc["Stripe"] + PayPal_svc["PayPal"] + Paymob_svc["Paymob"] + end + + Browser -->|HTTPS| Nginx + Nginx -->|"/app/*"| Browser + Nginx -->|"/api/*"| API + API --> Core + API --> Adaptive + API --> Exam + API --> LMS + API --> AImod + API --> Biz + AImod --> GPT + AImod --> Whisper + AImod --> Polly + AImod --> ELAI_svc + AImod --> GPTZero_svc + AImod --> FAISS_svc + Core --> PG + Adaptive --> PG + Exam --> PG + LMS --> PG + Biz --> PG + API <-->|"REST sync"| SIS + Biz --> Stripe_svc + Biz --> PayPal_svc + Biz --> Paymob_svc +``` + +### 2.2 Technology Stack + +| Layer | Technology | +|-------|-----------| +| **Frontend** | React 18, TypeScript, Vite 5, React Router v6, TanStack Query | +| **UI** | shadcn/ui (Radix primitives), Tailwind CSS 3, Recharts, Lucide icons | +| **Forms** | react-hook-form + Zod validation | +| **Backend** | Odoo 19, Python 3.11 | +| **Database** | PostgreSQL 16 | +| **LMS** | OpenEduCat (Odoo-native modules) | +| **AI** | OpenAI GPT-4o/3.5-turbo, Whisper (local base model), AWS Polly Neural, ELAI, GPTZero, FAISS + all-MiniLM-L6-v2 | +| **Deployment** | Docker Compose, Nginx reverse proxy | +| **Version Control** | Gitea | + +### 2.3 Multi-Tenant Model + +```mermaid +graph TB + subgraph tenants ["Tenant Types"] + UTAS["UTAS Entity
Institutional license
SIS-synced students
Curriculum-bound"] + Freelance["Freelance
No entity / self-entity
Self-registered
AI-generated plans
Individual subscription"] + Corp["Corporate Entities
Bulk licenses
Managed enrollment"] + end + + subgraph shared ["Shared Platform"] + SubjectEngine["Universal Subject Engine"] + ExamEngine["Exam Engine"] + AIServices["AI Services"] + LMSmod["LMS Module"] + end + + UTAS --> SubjectEngine + Freelance --> SubjectEngine + Corp --> SubjectEngine + SubjectEngine --> ExamEngine + SubjectEngine --> AIServices + SubjectEngine --> LMSmod +``` + +| Mode | Registration | Learning Plan | Content | Billing | +|------|-------------|---------------|---------|---------| +| **UTAS Student** | SIS sync or admin batch import | AI-generated, curriculum-aligned (topics from UTAS syllabus) | Manual resources from UTAS staff + AI supplementary | Institutional license | +| **Freelance Student** | Self-registration | Fully AI-generated (topic selection from global taxonomy) | AI-generated primary + curated community resources | Individual subscription (Stripe/PayPal/Paymob) | +| **Corporate Student** | Admin batch import or code-based | Teacher/admin assigned + adaptive | Entity-managed resources + AI supplementary | Corporate license | + +--- + +## 3. User Roles and Multi-Tenancy + +### 3.1 User Types + +| Type | Access | Key Capabilities | +|------|--------|-----------------| +| `student` | Student portal | Take exams, follow learning plans, view grades, attend classes, receive AI coaching | +| `teacher` | Teacher portal | Create courses, manage assignments, grade students, upload resources, view class analytics | +| `admin` | Full admin portal | Manage users/entities/classrooms, configure platform, generate exams, manage workflows | +| `corporate` | Corporate dashboard | Manage entity members, view entity statistics, bulk operations | +| `mastercorporate` | Multi-entity corporate | Cross-entity management and reporting | +| `agent` | Sales agent | Visible on landing page, sales operations | +| `developer` | Developer tools | System diagnostics, advanced configuration | + +### 3.2 Entity-Scoped Permissions + +All data access is scoped by entity membership. A user may belong to multiple entities with different roles in each. Permission checks combine **global user type** + **entity role permissions**. + +--- + +## 4. Authentication and Authorization + +> **Implementation:** `auth.service.ts` | `encoach_api/controllers/auth.py` | `Login.tsx`, `Register.tsx`, `ForgotPassword.tsx` + +### 4.1 Authentication + +| Method | Detail | +|--------|--------| +| **Primary** | Odoo JWT (HS256). Login returns access token; all API calls include `Authorization: Bearer `. | +| **Session** | Token stored in httpOnly cookie or localStorage (frontend decision). | +| **Registration** | Self-registration (freelance), batch import (institutional), code-based (corporate). | +| **Password Reset** | Email-based verification flow via `/api/reset` and `/api/reset/sendVerification`. | + +### 4.2 Authorization -- Entity Role Permissions + +77 entity-scoped permissions organized by category: + +| Category | Permissions | +|----------|-----------| +| **User Management** | `view_students`, `edit_students`, `delete_students`, `view_teachers`, `edit_teachers`, `delete_teachers`, `view_corporates`, `edit_corporates`, `delete_corporates`, `view_mastercorporates`, `edit_mastercorporates`, `delete_mastercorporates`, `create_user`, `create_user_batch` | +| **Exam Management** | `generate_{module}`, `view_{module}`, `delete_{module}` for each subject skill. Legacy: reading/listening/writing/speaking/level. New: parameterized by subject and skill via subject taxonomy. | +| **Classroom** | `view_classrooms`, `create_classroom`, `rename_classrooms`, `add_to_classroom`, `remove_from_classroom`, `delete_classroom`, `upload_classroom` | +| **Entity** | `view_entities`, `rename_entity`, `add_to_entity`, `remove_from_entity`, `delete_entity`, `view_entity_roles`, `create_entity_role`, `rename_entity_role`, `edit_role_permissions`, `assign_to_role`, `delete_entity_role`, `view_entity_statistics` | +| **Assignment** | `view_assignments`, `create_assignment`, `edit_assignment`, `delete_assignment`, `start_assignment`, `archive_assignment` | +| **Content** | `create_code`, `create_code_batch`, `view_code_list`, `delete_code`, `edit_grading_system` | +| **Reporting** | `view_statistics`, `download_statistics_report`, `view_student_performance`, `view_student_record`, `download_student_record`, `download_user_list` | +| **Financial** | `pay_entity`, `view_payment_record` | +| **Workflow** | `view_workflows`, `configure_workflows`, `edit_workflow`, `delete_workflow`, `view_approval_workflows`, `update_exam_privacy`, `view_confidential_exams`, `create_confidential_exams`, `create_public_exams` | + +### 4.3 Global Permissions + +Global permissions control page-level access independent of entity: + +| Topic | Permissions | +|-------|-----------| +| Manage Corporate | `viewCorporate`, `editCorporate`, `deleteCorporate`, `createCodeCorporate` | +| Manage Admin | `viewAdmin`, `editAdmin`, `deleteAdmin`, `createCodeAdmin` | +| Manage Student | `viewStudent`, `editStudent`, `deleteStudent`, `createCodeStudent` | +| Manage Teacher | `viewTeacher`, `editTeacher`, `deleteTeacher`, `createCodeTeacher` | +| Manage Exams | `createReadingExam`, `createListeningExam`, `createWritingExam`, `createSpeakingExam`, `createLevelExam` -- **extended per subject** | +| View Pages | `viewExams`, `viewExercises`, `viewRecords`, `viewStats`, `viewTickets`, `viewPaymentRecords` | +| Manage Groups | `viewGroup`, `editGroup`, `deleteGroup`, `createGroup` | +| Manage Codes | `viewCodes`, `deleteCodes`, `createCodes` | +| Super | `all` | + +### 4.4 Permission Extension for Multi-Subject (AD-5) + +The current permission model is IELTS-hardcoded (`generate_reading`, `createWritingExam`, etc.). For multi-subject support: + +**Strategy:** Add subject-parameterized permissions while maintaining backward compatibility. + +| Current (IELTS-only) | New (Universal) | Mapping | +|----------------------|----------------|---------| +| `generate_reading` | `generate_exam:english:reading` | Existing English exams auto-map | +| `generate_writing` | `generate_exam:english:writing` | Same | +| (new) | `generate_exam:math:algebra` | New subject permissions | +| (new) | `generate_exam:it:networking` | New subject permissions | +| (new) | `manage_taxonomy:{subject}` | Taxonomy administration per subject | +| (new) | `manage_resources:{subject}` | Resource upload per subject | +| (new) | `view_adaptive:{subject}` | View adaptive learning data per subject | + +--- + +## 5. Global UI Shell + +### 5.1 Navbar + +| ID | Requirement | Source | +|----|-------------|--------| +| FR-GNAV-01 | Brand/logo links to role-appropriate dashboard | Production | +| FR-GNAV-02 | Student: subject progress indicators (mastery % per enrolled subject) | New (replaces IELTS-only skill indicators) | +| FR-GNAV-03 | Help button opens ticket submission with page context | Production | +| FR-GNAV-04 | Subscription/expiry indicator; link to payment when applicable | Production | +| FR-GNAV-05 | Profile link to `/profile` | Production | +| FR-GNAV-06 | Mobile-responsive menu | Production | +| FR-GNAV-07 | AI Search Bar (global) -- keyword search with AI-powered suggestions, route to relevant pages | New frontend | +| FR-GNAV-08 | AI Assistant Drawer -- chat-style AI help accessible from any page | New frontend | + +### 5.2 Sidebar Navigation + +| ID | Requirement | +|----|-------------| +| FR-SIDE-01 | Entries appear only when permitted (user type + entity permissions) | +| FR-SIDE-02 | Sidebar minimize preference persisted locally | +| FR-SIDE-03 | Logout ends session and clears state | +| FR-SIDE-04 | Ticket badge shows assigned count | + +**Student sidebar:** Dashboard, Subjects, Courses, Subject Registration, Assignments, Grades, Attendance, Timetable, Discussions, Messages, Announcements, Journey, Profile + +**Teacher sidebar:** Dashboard, Courses, Assignments, Attendance, Students, Timetable, Discussions, Announcements, Profile + +**Admin sidebar (grouped):** + +| Group | Items | +|-------|-------| +| **LMS** | Dashboard, Courses, Students, Teachers, Batches, Timetable, Reports, Settings | +| **Platform** | Platform Dashboard | +| **Institutional** | Academic Years, Departments, Admissions, Exam Sessions, Marksheets | +| **Academic** | Assignments, Exams List, Exam Structures, Rubrics, Generation, Approval Workflows | +| **Management** | Users, Entities, Classrooms, Taxonomy Manager, Resource Manager | +| **Reports** | Student Performance, Stats Corporate, Record | +| **Training** | Vocabulary, Grammar | +| **Support** | Payment Record, Tickets, Settings | + +### 5.3 Breadcrumbs + +Inner pages show: Home > Section > Nested segments (e.g., Assignments > Creator, Training > Vocabulary). + +--- + +# Part II -- Universal Subject Engine + +## 6. Subject Taxonomy + +> **Implementation:** `taxonomy.service.ts` | `encoach_adaptive_api/controllers/taxonomy.py` | `AdminTaxonomy.tsx` + +### 6.1 Hierarchy + +``` +Subject (e.g., Mathematics) +└── Domain (e.g., Algebra) + └── Topic (e.g., Linear Equations) + └── Learning Objective (e.g., "Solve 2-variable systems") +``` + +Topics form a directed acyclic graph via prerequisite relationships. A student cannot begin a topic until all prerequisites reach Proficient (mastery >= 60%). + +### 6.2 IELTS / English Taxonomy + +The existing IELTS skills map into the universal taxonomy: + +``` +English / IELTS +├── Reading +│ ├── Skimming and Scanning (prerequisites: none) +│ ├── Detail Comprehension (prerequisites: Skimming) +│ ├── Inference and Deduction (prerequisites: Detail Comprehension) +│ └── Academic Reading (prerequisites: Inference) +├── Listening +│ ├── Section 1-2: Social/Everyday (prerequisites: none) +│ ├── Section 3-4: Academic (prerequisites: Section 1-2) +│ └── Note/Summary Completion (prerequisites: Section 1-2) +├── Writing +│ ├── Task 1 General: Letter (prerequisites: none) +│ ├── Task 1 Academic: Data Description (prerequisites: none) +│ └── Task 2: Essay (prerequisites: Task 1 General or Task 1 Academic) +├── Speaking +│ ├── Part 1: Introduction (prerequisites: none) +│ ├── Part 2: Long Turn (prerequisites: Part 1) +│ └── Part 3: Discussion (prerequisites: Part 2) +└── General English + ├── Grammar Foundations (prerequisites: none) + ├── Vocabulary Building (prerequisites: none) + └── Pronunciation (prerequisites: none) +``` + +### 6.3 Mathematics Taxonomy + +``` +Mathematics +├── Arithmetic +│ ├── Number Operations (prerequisites: none) +│ ├── Fractions and Decimals (prerequisites: Number Operations) +│ └── Ratios and Proportions (prerequisites: Fractions) +├── Algebra +│ ├── Algebraic Expressions (prerequisites: Number Operations) +│ ├── Linear Equations (prerequisites: Algebraic Expressions) +│ ├── Quadratic Equations (prerequisites: Linear Equations) +│ └── Polynomials (prerequisites: Quadratic Equations) +├── Geometry +│ ├── Basic Shapes (prerequisites: Number Operations) +│ ├── Angles and Triangles (prerequisites: Basic Shapes) +│ ├── Coordinate Geometry (prerequisites: Linear Equations, Basic Shapes) +│ └── Trigonometry (prerequisites: Angles, Coordinate Geometry) +├── Statistics and Probability +│ ├── Data Representation (prerequisites: Number Operations) +│ ├── Central Tendency (prerequisites: Data Representation) +│ └── Basic Probability (prerequisites: Fractions) +└── Calculus + ├── Limits (prerequisites: Polynomials, Coordinate Geometry) + ├── Derivatives (prerequisites: Limits) + └── Integrals (prerequisites: Derivatives) +``` + +### 6.4 Information Technology Taxonomy + +``` +Information Technology +├── Computer Fundamentals +│ ├── Hardware Components (prerequisites: none) +│ ├── Operating Systems (prerequisites: Hardware) +│ └── Number Systems (prerequisites: none) +├── Networking +│ ├── Network Basics (prerequisites: Hardware) +│ ├── TCP/IP Model (prerequisites: Network Basics) +│ ├── IP Addressing (prerequisites: TCP/IP, Number Systems) +│ └── Network Security (prerequisites: IP Addressing) +├── Databases +│ ├── Database Concepts (prerequisites: Operating Systems) +│ ├── SQL Fundamentals (prerequisites: Database Concepts) +│ ├── Database Design (prerequisites: SQL Fundamentals) +│ └── Advanced SQL (prerequisites: Database Design) +├── Programming +│ ├── Programming Logic (prerequisites: none) +│ ├── Python Basics (prerequisites: Programming Logic) +│ ├── Data Structures (prerequisites: Python Basics) +│ └── Object-Oriented Programming (prerequisites: Data Structures) +└── Cybersecurity + ├── Security Fundamentals (prerequisites: Network Security) + └── Cryptography Basics (prerequisites: Security Fundamentals, Number Systems) +``` + +### 6.5 Taxonomy Management + +| Action | Actor | API | +|--------|-------|-----| +| Create/edit subjects | Admin | `POST/PATCH /api/subjects` | +| Create/edit domains | Admin / Staff | `POST/PATCH /api/domains` | +| Create/edit topics with prerequisites | Staff | `POST/PATCH /api/topics` | +| Create/edit learning objectives | Staff | Nested in topic API | +| Bulk import taxonomy (JSON/CSV) | Admin | `POST /api/subjects/{id}/taxonomy/import` | +| AI-suggest sub-topics for a domain | Staff | `POST /api/domains/{id}/ai-suggest` | + +--- + +## 7. Diagnostic Assessment + +> **Implementation:** `adaptive.service.ts` | `encoach_adaptive_api/controllers/diagnostic.py` | `StudentDiagnostic.tsx` + +### 7.1 Purpose + +When a student begins a new subject, a diagnostic assessment determines their per-topic proficiency. This is the input for learning plan generation. For English, this replaces/extends the existing Level Test. + +### 7.2 Adaptive Algorithm + +The diagnostic uses Computer Adaptive Testing: starts at medium difficulty, adjusts up on correct answers, down on incorrect. Cycles through domains round-robin. + +| Parameter | Default | Configurable | +|-----------|---------|-------------| +| `questions_per_domain` | 4 | Yes (per subject) | +| `total_question_cap` | 25 | Yes | +| `time_limit_minutes` | 45 | Yes | +| `starting_difficulty` | `medium` | Yes | +| `mastery_per_correct` | +25 | Yes | +| `mastery_per_incorrect` | -10 | Yes | + +### 7.3 Question Types per Subject + +| Subject | Question Types | +|---------|---------------| +| **English** | Multiple choice, fill blanks, short answer, essay (writing), oral (speaking) | +| **Math** | Multiple choice, numerical (tolerance-graded), short answer, fill blanks, worked problem (multi-step) | +| **IT** | Multiple choice, true/false, short answer, code completion, scenario-based | + +### 7.4 Integration with Existing Exam Model + +Diagnostics reuse `encoach.exam` with `is_diagnostic = True` and a new `subject_id` field. `MODULE_SELECTION` extended with `math_diagnostic`, `it_diagnostic`. The existing `level` module maps to `english_diagnostic`. + +--- + +## 8. Proficiency Profile + +> **Implementation:** `adaptive.service.ts` | `encoach_adaptive_api/controllers/proficiency.py` | `StudentProficiency.tsx` + +### 8.1 Mastery Levels + +| Level | Score Range | Color | +|-------|-----------|-------| +| Not Started | 0-19% | Grey | +| Beginner | 20-39% | Red | +| Developing | 40-59% | Orange | +| Proficient | 60-79% | Yellow | +| Mastered | 80-100% | Green | + +### 8.2 Profile Updates + +| Trigger | Update Logic | +|---------|-------------| +| Diagnostic | Initial scores set per topic | +| Practice exercises | +5 correct, -2 incorrect | +| Mastery quiz | Pass (>= threshold): topic mastered. Fail: weighted average recalculation. | +| Time decay | -5% per week of inactivity after 30 days, floor at last quiz score minus 20% | + +--- + +## 9. Learning Plan Generation + +> **Implementation:** `adaptive.service.ts` | `encoach_adaptive_api/controllers/learning_plan.py` | `StudentLearningPlan.tsx` + +### 9.1 Two Modes + +| Mode | UTAS Student | Freelance Student | +|------|-------------|-------------------| +| **Scope** | Topics from UTAS curriculum for enrolled courses | All topics in selected subject | +| **Constraints** | Semester dates, course schedule | Self-set target date or open-ended | +| **Content** | UTAS-uploaded resources + AI supplementary | AI-generated primary + community resources | +| **Prerequisite enforcement** | Yes, with teacher override | Yes, no override | +| **Plan visibility** | Student + assigned teacher + admin | Student only | + +### 9.2 Algorithm + +1. **Topological sort** -- order topics respecting prerequisites +2. **Filter mastered** -- exclude topics with mastery >= threshold +3. **Prioritize** -- lowest mastery first, balanced across domains +4. **Estimate duration** -- based on topic complexity and current mastery +5. **AI refinement** -- GPT-4o generates motivational summary and specific advice + +### 9.3 Plan Adjustments + +| Trigger | Action | +|---------|--------| +| Faster mastery than estimated | Unlock next topic early | +| 3+ failed mastery quizzes | Additional resources, AI coaching intensified | +| Teacher override (UTAS) | Reorder, add/remove topics | +| 7+ days inactive | Notification; 14+ days pauses plan | + +--- + +## 10. Hybrid Content Delivery + +> **Implementation:** `adaptive.service.ts`, `resources.service.ts`, `coaching.service.ts` | `encoach_adaptive_api/controllers/content.py`, `encoach_resources/controllers/resources_rest.py` | `StudentTopic.tsx` + +### 10.1 Content Resolution Order + +1. **Human-uploaded resources** tagged to the topic (PDFs, videos, links) +2. **AI-generated content** if no human resources exist (explanations, worked examples, summaries) +3. **Content gap flag** if neither is available -- notifies admin + +### 10.2 Resource Types + +| Type | Format | Upload | +|------|--------|--------| +| PDF | `.pdf` | File upload | +| Video | URL (YouTube, Vimeo) or file upload | URL or multipart | +| External Link | Any URL | URL field | +| Document | `.docx`, `.pptx` | File upload | +| Interactive | Embedded HTML | URL field | + +### 10.3 AI Coaching + +| Action | Backend Service | Trigger | +|--------|----------------|---------| +| Answer explanation | GPT-4o | After each practice question | +| Hint | GPT-4o | Student requests during practice | +| Study suggestion | GPT-4o | After completing a topic section | +| Weakness analysis | GPT-4o | After failing mastery quiz | +| Formula reference (Math) | Static + GPT-4o | On demand | +| Motivational nudge | GPT-4o | 3+ days inactive | + +--- + +## 11. Progress Tracking and Mastery + +> **Implementation:** `adaptive.service.ts`, `analytics.service.ts` | `encoach_adaptive_api/controllers/content.py`, `encoach_api/controllers/ai_analytics.py` | `StudentJourney.tsx`, `AdminAdaptiveAnalytics.tsx` + +### 11.1 Mastery Quiz + +| Parameter | Value | +|-----------|-------| +| Questions | 5-10 (configurable per subject) | +| Time limit | 15 minutes (configurable) | +| Pass threshold | 80% (configurable per subject) | +| Attempts | Unlimited, new questions each time | +| Cooldown | 1 hour between attempts | +| On pass | Topic completed, next unlocked | +| On fail | Mastery recalculated, AI coaching triggered | + +### 11.2 Spaced Repetition + +| Days Since Mastery | Review | +|-------------------|--------| +| 7 | 3 quick-recall questions | +| 30 | 5 questions at original difficulty | +| 90 | Full mastery quiz equivalent | + +### 11.3 Analytics + +**Student:** Overall mastery %, domain radar chart, plan progress, study streak, upcoming reviews. + +**Teacher:** Class mastery heatmap, at-risk students, content gap report, time per topic vs estimate. + +**Admin:** Subject-level stats (enrollments, completion rates, avg mastery), AI usage metrics, resource utilization. + +--- + +# Part III -- LMS Module + +## 12. Course Management + +> **Implementation:** `lms.service.ts` | `encoach_lms_api/controllers/courses.py` | `StudentCourses.tsx`, `TeacherCourses.tsx`, `AdminCourses.tsx` + +### 12.1 Overview + +Courses are managed through OpenEduCat models exposed via REST API. The frontend provides student, teacher, and admin course views. + +### 12.2 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-CRS-01 | List enrolled courses with progress | `/student/courses` | Student | +| FR-CRS-02 | View course detail with modules and lessons | `/student/courses/:id` | Student | +| FR-CRS-03 | List managed courses | `/teacher/courses` | Teacher | +| FR-CRS-04 | Create new course with modules and lessons | `/teacher/courses/new` | Teacher | +| FR-CRS-05 | Edit existing course | `/teacher/courses/:id/edit` | Teacher | +| FR-CRS-06 | Admin course management (all courses, all entities) | `/admin/courses` | Admin | + +### 12.3 Course Data Model + +| Field | Type | Description | +|-------|------|-------------| +| title | String | Course title | +| code | String | Course code (e.g., "MATH101") | +| subject_id | Reference | Link to subject taxonomy | +| instructor_id | Reference | Assigned teacher | +| description | Text | Course description | +| level | Selection | Beginner, Intermediate, Advanced | +| modules | One2many | Course modules (units) | +| max_capacity | Integer | Enrollment limit | +| start_date / end_date | Date | Course period | +| status | Selection | draft, active, archived | + +--- + +## 13. Batch and Enrollment + +> **Implementation:** `lms.service.ts` | `encoach_lms_api/controllers/batches.py` | `AdminBatches.tsx` + +| ID | Requirement | Route | +|----|-------------|-------| +| FR-BATCH-01 | List batches with student counts | `/admin/batches` | +| FR-BATCH-02 | View batch detail with enrolled students | `/admin/batches/:id` | +| FR-BATCH-03 | Create batch, assign students and courses | Admin | +| FR-BATCH-04 | Enrollment: student self-enrollment (freelance) or admin assignment (UTAS) | API | + +--- + +## 14. Timetable and Attendance + +> **Implementation:** `lms.service.ts` | `encoach_lms_api/controllers/timetable.py`, `encoach_lms_api/controllers/attendance.py` | `StudentTimetable.tsx`, `TeacherTimetable.tsx`, `AdminTimetable.tsx`, `StudentAttendance.tsx`, `TeacherAttendance.tsx` + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-TT-01 | View personal timetable | `/student/timetable` | Student | +| FR-TT-02 | View teaching timetable | `/teacher/timetable` | Teacher | +| FR-TT-03 | Manage timetable (create, edit sessions) | `/admin/timetable` | Admin | +| FR-ATT-01 | View personal attendance | `/student/attendance` | Student | +| FR-ATT-02 | Record attendance for class | `/teacher/attendance` | Teacher | + +--- + +## 15. Gradebook and Reports + +> **Implementation:** `lms.service.ts`, `analytics.service.ts` | `encoach_lms_api/controllers/grades.py`, `encoach_api/controllers/ai_analytics.py` | `StudentGrades.tsx`, `AdminReports.tsx` + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-GRD-01 | View personal grades across courses | `/student/grades` | Student | +| FR-GRD-02 | Grade student submissions | `/teacher/assignments/:id` | Teacher | +| FR-RPT-01 | Academic reports with charts | `/admin/reports` | Admin | +| FR-RPT-02 | AI-generated report narratives | `/admin/reports` (AiReportNarrative) | Admin | + +--- + +# Part IV -- Exam Engine + +## 16. Exam Management + +> **Implementation:** `exams.service.ts` | `encoach_api/controllers/exam.py` | `AdminExamsList.tsx`, `AdminExamStructures.tsx`, `AdminRubrics.tsx`, `Exam.tsx` + +### 16.1 Exam CRUD (Production Parity) + +| ID | Requirement | Source | +|----|-------------|--------| +| FR-EX-01 | Filter exams by module (extended: by subject and skill) | Production | +| FR-EX-02 | Load exam by Exam ID | Production | +| FR-EX-03 | Text search | Production | +| FR-EX-04 | Pagination and page size | Production | +| FR-EX-05 | Columns: title, creator, module, rubrics, difficulty, exercises, timer, access, created at | Production | +| FR-EX-06 | Row actions: set public/private, edit, load, delete | Production | +| FR-EX-07 | **NEW:** Subject filter (English, Math, IT) alongside module filter | New | +| FR-EX-08 | **NEW:** Exercise vs Exam differentiation -- `is_exercise` flag; exercises are self-paced practice (no journey tracking), exams are official graded assessments | Client | +| FR-EX-09 | **NEW:** `is_official` flag on exams -- official exams track to student journey and support special access modes | Client | +| FR-EX-10 | **NEW:** Official exam access modes: (1) unique shareable link, (2) landing page access, (3) separate login screen | Client | +| FR-EX-11 | **NEW:** Exam activation control -- teacher sets start date, end date, activation status | Client | +| FR-EX-12 | **NEW:** Different exam for each student option (AI generates unique variants per student) | Client | +| FR-EX-13 | **NEW:** Configurable reminder frequency -- number of reminders, notification via email and system notification | Client | +| FR-EX-14 | **NEW:** Exam suspend/revoke actions | Client | +| FR-EX-15 | **NEW:** Student inquiry and extension requests on exams | Client | + +### 16.2 Official Exam Access (NEW) + +| Access Mode | Description | Route | +|-------------|-------------|-------| +| **Link Access** | Teacher generates a unique URL; students access exam via the link without full platform login | `/exam/access/{token}` | +| **Landing Page** | Exam appears on the public landing page; students authenticate with exam code + credentials | `/exam/official/{code}` | +| **Separate Login** | Dedicated login screen for the exam only; no access to other platform features | `/exam/login/{examId}` | + +### 16.4 Rubrics (Production Parity) + +| ID | Requirement | +|----|-------------| +| FR-RUB-01-05 | Search, pagination, create rubric, filter dropdown, metadata and edit | +| FR-RG-01-05 | Rubric groups: search, pagination, create, filter, edit/delete | + +### 16.5 Exam Structures (Production Parity) + +| ID | Requirement | +|----|-------------| +| FR-ES-01 | Entity selector | +| FR-ES-02 | Text search | +| FR-ES-03 | Create structure (extended: per subject) | +| FR-ES-04 | Delete structure(s) | +| FR-ES-05 | Pagination | + +--- + +## 17. AI Content Generation + +> **Implementation:** `generation.service.ts` | `encoach_api/controllers/generation.py` | `AdminGeneration.tsx` + +### 17.1 Generation per Subject + +| Subject | AI Generates | Prompt Model | +|---------|-------------|-------------| +| **English** | Reading passages, listening scripts, writing tasks, speaking questions, level test exercises | GPT-4o (temp 0.7 for generation) | +| **Math** | Questions per topic (MCQ, numerical, word problems, worked problems) | GPT-4o with LaTeX output | +| **IT** | Questions per topic (MCQ, true/false, code completion, scenarios) | GPT-4o with code block output | + +### 17.2 Generation Workflow + +| ID | Requirement | Source | +|----|-------------|--------| +| FR-GEN-01 | Select title, label, entity, subject, and skill/module | Production (extended) | +| FR-GEN-02 | AI generates exam content via GPT-4o | Production | +| FR-GEN-03 | **NEW:** AI Creation Assistant -- modal flow for "Create with AI" for courses, exams, rubrics, assignments | New frontend (AiCreationAssistant) | +| FR-GEN-04 | **NEW:** AI Generator Modal -- configurable generation parameters | New frontend (AiGeneratorModal) | + +--- + +## 18. Exam Delivery + +> **Implementation:** `exams.service.ts`, `media.service.ts` | `encoach_api/controllers/exam.py`, `encoach_api/controllers/generation.py` | `Exam.tsx`, `OfficialExam.tsx` + +### 18.1 Delivery Modes + +| Mode | Route | Chrome | +|------|-------|--------| +| Practice | `/exam` | Full UI with navigation | +| Assignment | `/exam?assignment={id}` | Reduced chrome | +| Official | `/official-exam` | Minimal chrome, strict timer | +| Diagnostic | `/student/diagnostic/:subjectId` | Adaptive, one question at a time | +| Mastery Quiz | `/student/mastery-quiz/:topicId` | Standard quiz, fresh questions | + +### 18.2 Media Integration + +| Media | Service | Usage | +|-------|---------|-------| +| Listening audio | AWS Polly TTS | English listening sections (11 neural voices) | +| Speaking video | ELAI avatars | English speaking prompts (7 avatars) | +| Speaking transcription | Whisper STT | Student audio transcription for grading | + +--- + +## 19. AI Grading + +> **Implementation:** `evaluations.service.ts`, `plagiarism.service.ts` | `encoach_api/controllers/generation.py`, `encoach_lms_api/controllers/plagiarism.py` | `AdminApprovalWorkflows.tsx` + +### 19.1 Grading per Subject + +| Subject | Grading Type | Method | +|---------|-------------|--------| +| English Writing | Rubric-based (IELTS band scoring) | GPT-4o (temp 0.1) evaluates Task Achievement, Coherence, Lexical Resource, Grammar | +| English Speaking | Rubric-based (IELTS band scoring) | Whisper transcription + GPT-4o grading | +| English Short Answer | Passage-based | GPT-4o evaluates against correct answers | +| Math MCQ | Exact match | Direct comparison | +| Math Numerical | Tolerance-based | `abs(student - correct) <= tolerance` | +| Math Worked Problem | Step evaluation | GPT-4o evaluates methodology and answer | +| IT MCQ / True-False | Exact match | Direct comparison | +| IT Code Completion | AI evaluation | GPT-4o evaluates correctness, approach, quality | +| IT Scenario | AI evaluation | GPT-4o evaluates against rubric | +| All Writing | AI detection | GPTZero per-sentence analysis | + +### 19.2 Plagiarism Detection (NEW -- Implemented) + +> **Implementation:** `plagiarism.service.ts` | `encoach_lms_api/controllers/plagiarism.py` + +| ID | Requirement | Source | +|----|-------------|--------| +| FR-PLAG-01 | Enable/disable plagiarism check per assignment or exam | Client | +| FR-PLAG-02 | Automatic GPTZero analysis on submission | Client | +| FR-PLAG-03 | Downloadable plagiarism report (PDF) per submission | Client | +| FR-PLAG-04 | Configurable number of allowed submissions with plagiarism check | Client | +| FR-PLAG-05 | Teacher views plagiarism score and per-sentence analysis in grading interface | Client | +| FR-PLAG-06 | Aggregate plagiarism overview for assignment (flagged submissions count) | Client | + +**API Endpoints:** + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/api/plagiarism/check` | Run plagiarism check on text/submission | +| `GET` | `/api/plagiarism/report/{submissionId}` | Get plagiarism report | +| `GET` | `/api/plagiarism/report/{submissionId}/download` | Download report as PDF | + +### 19.3 Approval Workflows (Enhanced) + +| ID | Requirement | Source | +|----|-------------|--------| +| FR-AWF-01 | List/filter workflows | Production | +| FR-AWF-02 | Create templates / add workflow | Production | +| FR-AWF-03 | **NEW:** AI Grading Assistant on workflow page | New frontend | +| FR-AWF-04 | **NEW:** Configurable approval stages with number of days per stage (shown as calendar) | Client | +| FR-AWF-05 | **NEW:** Auto-escalation -- if time passes, automatically move to next authority with highlighted reason | Client | +| FR-AWF-06 | **NEW:** Bypass option -- terminate approval flow with mandatory justification sent to final authority | Client | +| FR-AWF-07 | **NEW:** Comments saved through the approval process | Client | +| FR-AWF-08 | **NEW:** Status view of approval progress (first reviewer, rejected, final reviewer, etc.) | Client | +| FR-AWF-09 | **NEW:** Final authority can choose who to pass back the exam to | Client | +| FR-AWF-10 | **NEW:** Configurable email notifications per approval stage | Client | +| FR-AWF-11 | **NEW:** Approval applies to both exams and course materials | Client | + +**Approval Stage Model:** + +| Field | Type | Description | +|-------|------|-------------| +| `workflow_id` | Many2one | Parent workflow | +| `sequence` | Integer | Stage order | +| `approver_id` | Many2one | Approver user | +| `max_days` | Integer | Maximum days before auto-escalation | +| `notification_email` | Char | Email for notifications | +| `status` | Selection | `pending`, `approved`, `rejected`, `escalated`, `bypassed` | +| `comment` | Text | Reviewer comment | +| `acted_at` | Datetime | When action was taken | + +--- + +# Part V -- AI Services + +## 20. AI Services Catalog + +> **Implementation:** `coaching.service.ts`, `analytics.service.ts`, `generation.service.ts`, `evaluations.service.ts`, `media.service.ts` | `encoach_ai/`, `encoach_ai_generation/`, `encoach_ai_grading/`, `encoach_ai_media/`, `encoach_adaptive_ai/` + +| Service | Model | Config | Usage | +|---------|-------|--------|-------| +| **OpenAI GPT-4o** | gpt-4o (primary), gpt-3.5-turbo (secondary) | Temp: 0.1 grading, 0.7 generation, 0.2 summaries. Max tokens: 4,097. JSON format. | Exam generation, grading, evaluation, training tips, learning plans, coaching, content generation | +| **Whisper** | base (local, ~1GB) | 4 instances, round-robin, 16kHz mono, 30s chunks | Speaking transcription, audio-to-text | +| **AWS Polly** | Neural engine | 11 voices, MP3, 3,000 char chunks | Listening exam audio | +| **ELAI** | 7 avatars | ElevenLabs + Azure voices, fade_in animation | Speaking exam video, virtual tutoring | +| **GPTZero** | v2/predict/text | Per-sentence analysis, confidence scoring | Writing AI detection | +| **FAISS** | IndexFlatL2 + all-MiniLM-L6-v2 | Top-5, per-subject indices | Training tips, content recommendations | + +--- + +## 21. AI Integration in Frontend + +Each simulated AI component in the new frontend maps to a real backend service: + +| Frontend Component | Mock Behavior | Real Backend Integration | +|-------------------|--------------|--------------------------| +| `AiAssistantDrawer` | Canned responses via `mockResponses` map | GPT-4o conversational API. Context: current page, student profile, recent activity. Endpoint: `POST /api/coach/chat` | +| `AiSearchBar` | Keyword-to-route mapping | GPT-4o semantic search + FAISS. Returns relevant content, pages, and actions. Endpoint: `POST /api/ai/search` | +| `AiCreationAssistant` | Simulated creation flows | GPT-4o generates course outlines, exam content, rubrics, assignments based on type parameter. Endpoints: existing `/api/exam/*/generate` + new `/api/courses/ai-generate` | +| `AiGeneratorModal` | Simulated with setTimeout | Real generation call to GPT-4o with progress streaming. Endpoint: existing generation endpoints | +| `AiGradingAssistant` | Fixed suggested marks | Real grading via `POST /api/evaluate/writing` or `/api/evaluate/speaking` or `/api/grading/multiple`. Returns scores + feedback | +| `AiWritingHelper` | `useAiSimulation` delay | GPT-4o writing assistance. Endpoint: `POST /api/coach/writing-help` | +| `AiGradeExplainer` | Static explanation | GPT-4o explains grade breakdown. Endpoint: `POST /api/coach/explain` | +| `AiStudyCoach` | Rotating static tips | Real adaptive engine: proficiency profile + FAISS tips + GPT-4o recommendations. Endpoint: `POST /api/coach/suggest` | +| `AiInsightsPanel` | Static insight cards | GPT-4o analysis of student/class data. Endpoint: `POST /api/ai/insights` | +| `AiTipBanner` | Static tips | Context-aware tips from FAISS + GPT-4o. Endpoint: `GET /api/coach/tip?context={page}` | +| `AiAlertBanner` | Static alerts | Rule-based alerts (attendance, grades) + GPT-4o severity assessment. Endpoint: `GET /api/ai/alerts` | +| `AiReportNarrative` | Static narrative | GPT-4o generates report narrative from analytics data. Endpoint: `POST /api/ai/report-narrative` | +| `AiRiskBadge` | Heuristic from mock data | Risk calculation from attendance + grades + engagement. Endpoint: included in student profile API | +| `AiBatchOptimizer` | Static suggestions | GPT-4o analyzes batch composition and suggests optimizations. Endpoint: `POST /api/ai/batch-optimize` | +| `useAiSimulation` | setTimeout + canned response | Replaced by real TanStack Query mutations to backend APIs | + +--- + +# Part VI -- Administration + +## 22. User Management (Production Parity) + +> **Implementation:** `users.service.ts` | `encoach_api/controllers/users.py` | `AdminUsers.tsx` + +| ID | Requirement | Route | +|----|-------------|-------| +| FR-USR-01 | Filter by entity | `/users?type=student\|teacher\|corporate\|mastercorporate` | +| FR-USR-02 | Text search (name/email) | Same | +| FR-USR-03 | Download/export | Same | +| FR-USR-04 | Pagination and page size | Same | +| FR-USR-05 | Type-specific columns (name, email, type, entities, classrooms, student ID, verified) | Same | +| FR-USR-06 | **NEW:** Subject enrollment status per student | New | + +--- + +## 23. Entity Management (Production Parity) + +> **Implementation:** `entities.service.ts` | `encoach_api/controllers/entities.py` | `AdminEntities.tsx` + +| ID | Requirement | +|----|-------------| +| FR-ENT-01 | Search and pagination | +| FR-ENT-02 | Create entity (label, licenses/capacity, member selection) | +| FR-ENT-03 | Cards: usage/capacity, role counts | +| FR-ENT-04 | Role and permission management per entity | + +--- + +## 24. Classroom Management (Production Parity) + +> **Implementation:** `classrooms.service.ts` | `encoach_api/controllers/classroom.py` | `AdminClassrooms.tsx` + +| ID | Requirement | +|----|-------------| +| FR-CL-01 | Entity filter and search | +| FR-CL-02 | Cards: name, admin, entity, participant count | +| FR-CL-03 | Create classroom | +| FR-CL-04 | Transfer students across classrooms | +| FR-CL-05 | Delete / bulk delete | + +--- + +## 25. Assignment Management (Production Parity) + +> **Implementation:** `assignments.service.ts` | `encoach_api/controllers/assignment.py` | `AdminAssignments.tsx`, `StudentAssignments.tsx`, `TeacherAssignments.tsx` + +| ID | Requirement | +|----|-------------| +| FR-ASG-01 | Tabs: Active, Planned, Past, Start Expired, Archived with counts | +| FR-ASG-02 | Create assignment (entity, dates, classrooms, assignees, exams) | +| FR-ASG-03 | Search and pagination (`state`, `page`, `size`) | +| FR-ASG-04 | Breadcrumb navigation | +| FR-ASG-05 | Assignment execution via `/exam?assignment={id}` | +| FR-ASG-06 | **NEW:** Subject-scoped assignments (assign Math exams, IT exams, not just IELTS) | +| FR-ASG-07 | **NEW:** Late submission handling -- configurable late submission date/time with penalty structure | Client | +| FR-ASG-08 | **NEW:** Extension requests -- students can request deadline extensions through the platform | Client | +| FR-ASG-09 | **NEW:** Submission tracking -- status flow (start date, extension granted, submitted, in review, graded) | Client | +| FR-ASG-10 | **NEW:** Configurable number of allowed submissions per assignment | Client | +| FR-ASG-11 | **NEW:** Revision history -- track all submission versions per student | Client | +| FR-ASG-12 | **NEW:** Reminder notifications -- configurable frequency, sent via email and system notification | Client | + +### 25.2 Late Submission Policy + +| Field | Type | Description | +|-------|------|-------------| +| `late_deadline` | Datetime | Final late submission cutoff | +| `penalty_type` | Selection | `none`, `percentage`, `fixed_deduction` | +| `penalty_value` | Float | Penalty amount (e.g., 10 = 10% or 10 points) | +| `max_submissions` | Integer | Maximum allowed submissions (0 = unlimited) | +| `allow_extensions` | Boolean | Whether students can request extensions | + +### 25.3 Extension Request + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Requesting student | +| `assignment_id` | Many2one | Target assignment | +| `reason` | Text | Justification | +| `requested_date` | Datetime | Requested new deadline | +| `status` | Selection | `pending`, `approved`, `rejected` | +| `approved_by` | Many2one | Approving teacher | +| `response_note` | Text | Teacher response | + +--- + +## 26. Permission Management (Production Parity + Extension) + +> **Implementation:** `entities.service.ts` (roles/permissions) | `encoach_api/controllers/roles.py` | `RolesPermissions.tsx`, `AuthorityMatrix.tsx`, `UserRoles.tsx` + +| ID | Requirement | +|----|-------------| +| FR-PERM-01 | Navigate permission types at `/permissions` | +| FR-PERM-02 | View/edit permission details at `/permissions/:id` | +| FR-PERM-03 | **NEW:** Subject-parameterized permissions in UI (select subject, then skill permissions) | + +### 26.2 Roles and Permissions CRUD (NEW -- Implemented) + +The developer implemented a complete roles and permissions CRUD system beyond the original entity-scoped model. + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-ROLE-01 | List all roles with permission counts | `/admin/roles-permissions` | Admin | +| FR-ROLE-02 | Create role with name and description | `/admin/roles-permissions` | Admin | +| FR-ROLE-03 | Update role details | `/admin/roles-permissions` | Admin | +| FR-ROLE-04 | Delete role | `/admin/roles-permissions` | Admin | +| FR-ROLE-05 | Update role permissions (assign/remove permissions) | `/admin/roles-permissions` | Admin | +| FR-ROLE-06 | List all permissions | `/admin/roles-permissions` | Admin | +| FR-ROLE-07 | Create custom permission | `/admin/roles-permissions` | Admin | +| FR-ROLE-08 | Delete permission | `/admin/roles-permissions` | Admin | + +**API Endpoints:** + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/roles` | List all roles | +| `POST` | `/api/roles` | Create role | +| `GET` | `/api/roles/{id}` | Get role detail | +| `PATCH` | `/api/roles/{id}` | Update role | +| `DELETE` | `/api/roles/{id}` | Delete role | +| `PATCH` | `/api/roles/{id}/permissions` | Update role permissions | +| `GET` | `/api/permissions` | List all permissions | +| `POST` | `/api/permissions` | Create permission | +| `DELETE` | `/api/permissions/{id}` | Delete permission | + +### 26.3 User Role Assignment (NEW -- Implemented) + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-UROLE-01 | List users with their assigned roles | `/admin/user-roles` | Admin | +| FR-UROLE-02 | View roles for a specific user | `/admin/user-roles` | Admin | +| FR-UROLE-03 | Assign/remove roles for a user | `/admin/user-roles` | Admin | +| FR-UROLE-04 | Toggle individual role on/off for a user | `/admin/user-roles` | Admin | + +**API Endpoints:** + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/users/with-roles` | List users with role details | +| `GET` | `/api/users/{id}/roles` | Get roles for a user | +| `PATCH` | `/api/users/{id}/roles` | Update user roles | +| `POST` | `/api/users/{id}/roles/toggle` | Toggle a single role | + +### 26.4 Authority Matrix (NEW -- Implemented) + +A visual matrix showing all roles vs. all permissions, allowing administrators to toggle permission assignments visually. + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-AUTH-01 | Display role-permission matrix (roles as columns, permissions as rows) | `/admin/authority-matrix` | Admin | +| FR-AUTH-02 | Toggle individual permission cells (grant/revoke) | `/admin/authority-matrix` | Admin | +| FR-AUTH-03 | View user-specific authority matrix | `/admin/authority-matrix` | Admin | + +**API Endpoints:** + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/authority-matrix` | Get full role-permission matrix | +| `POST` | `/api/authority-matrix/toggle` | Toggle a permission cell | +| `GET` | `/api/user-authority-matrix` | Get user-level authority view | + +--- + +# Part VII -- Support and Commercial + +## 27. Ticketing System (Production Parity) + +> **Implementation:** `tickets.service.ts` | `encoach_api/controllers/ticket.py` | `AdminTickets.tsx` + +| ID | Requirement | +|----|-------------| +| FR-TCK-01 | Filters: Status, Type, Assignee, Source (All/Webpage/Platform) | +| FR-TCK-02 | Sort by date | +| FR-TCK-03 | Columns: ID, Type, Reporter, Source, Date, Subject, Status, Assignee, Corporate | +| FR-TCK-04 | Pagination | + +--- + +## 28. Subscriptions and Payments + +> **Implementation:** `subscriptions.service.ts` | `encoach_api/controllers/subscription.py` | `AdminPaymentRecord.tsx`, `Payment.tsx` + +### 28.1 Billing Models + +| Model | Description | Payment | +|-------|-------------|---------| +| **Institutional License** | UTAS or corporate entity purchases bulk licenses | Invoice-based or Stripe/PayPal | +| **Individual Subscription** | Freelance student purchases access per subject or platform-wide | Stripe, PayPal, or Paymob | +| **Package** | Predefined bundles (e.g., "IELTS Complete", "Math + IT Bundle") | Per-package pricing | + +### 28.2 Functional Requirements (Production Parity) + +| ID | Requirement | Route | +|----|-------------|-------| +| FR-PAY-01 | Payment page with entity context | `/payment` | +| FR-PAY-02 | Payment record / ledger | `/payment-record` | +| FR-PAY-03 | Package listing | `/api/packages` | +| FR-PAY-04 | Stripe checkout flow | `/api/stripe` | +| FR-PAY-05 | PayPal checkout flow | `/api/paypal` | +| FR-PAY-06 | Paymob checkout flow | `/api/paymob` | +| FR-PAY-07 | Webhook handling (Stripe, PayPal, Paymob) | `/api/*/webhook` | + +--- + +## 29. Training Content + +> **Implementation:** `training.service.ts` | `encoach_api/controllers/training.py` | `AdminTrainingVocabulary.tsx`, `AdminTrainingGrammar.tsx` + +### 29.1 Existing (Production Parity) + +| ID | Requirement | Route | +|----|-------------|-------| +| FR-TRN-01 | Training hub with entity/user/time filters | `/training` | +| FR-TRN-02 | Vocabulary training (progress, recommended, sections) | `/training/vocabulary` | +| FR-TRN-03 | Grammar training (parallel structure) | `/training/grammar` | +| FR-TRN-04 | Record view with entity/user/time/assignment filters | `/record` | + +### 29.2 New (Subject-Specific Tips) + +| ID | Requirement | +|----|-------------| +| FR-TRN-05 | **NEW:** Math-specific training tips via FAISS (algebra, geometry, statistics indices) | +| FR-TRN-06 | **NEW:** IT-specific training tips via FAISS (networking, databases, programming indices) | +| FR-TRN-07 | **NEW:** Personalized post-exam tips using subject-specific FAISS + GPT-4o selection | + +--- + +# Part VIII -- Institutional LMS (OpenEduCat) + +All LMS features in this part are backed by OpenEduCat community modules (ported to Odoo 19). The `encoach_lms_api` module extends these models and exposes them via REST API to the React frontend. + +--- + +## 30. Academic Year and Term Management + +> **Implementation:** `academic.service.ts` | `encoach_lms_api/controllers/academic.py` | `AdminAcademicYears.tsx` + +### 30.1 Overview + +Academic years define the institutional calendar. Each year contains terms (semesters, quarters) that govern course scheduling, batch periods, and exam sessions. + +### 30.2 Data Models (OpenEduCat) + +**`op.academic.year`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Academic year name (e.g., "2026-2027") | +| `start_date` | Date | Year start | +| `end_date` | Date | Year end | +| `term_structure` | Selection | `two_sem`, `two_sem_qua`, `three_sem`, `four_Quarter`, `final_year`, `others` | +| `academic_term_ids` | One2many | Terms within this year | + +**`op.academic.term`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Term name (e.g., "Semester 1") | +| `term_start_date` | Date | Term start | +| `term_end_date` | Date | Term end | +| `academic_year_id` | Many2one | Parent academic year | +| `parent_term` | Many2one | Parent term (for quarters within semesters) | + +### 30.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-AY-01 | List academic years with terms | `/admin/academic-years` | Admin | +| FR-AY-02 | Create academic year with term structure selection | `/admin/academic-years` | Admin | +| FR-AY-03 | Auto-generate terms based on selected structure | `/admin/academic-years` | Admin | +| FR-AY-04 | Edit/delete academic years and terms | `/admin/academic-years` | Admin | +| FR-AY-05 | Link batches and courses to academic terms | Related pages | Admin | + +### 30.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/academic-years` | List academic years | +| `POST` | `/api/academic-years` | Create academic year | +| `GET` | `/api/academic-years/{id}` | Get year with terms | +| `PATCH` | `/api/academic-years/{id}` | Update year | +| `DELETE` | `/api/academic-years/{id}` | Delete year | +| `POST` | `/api/academic-years/{id}/generate-terms` | Auto-generate terms from structure | +| `GET` | `/api/academic-terms` | List terms (filter by year) | +| `POST` | `/api/academic-terms` | Create term | +| `PATCH` | `/api/academic-terms/{id}` | Update term | +| `DELETE` | `/api/academic-terms/{id}` | Delete term | + +--- + +## 31. Department Management + +> **Implementation:** `academic.service.ts` | `encoach_lms_api/controllers/departments.py` | `AdminDepartments.tsx` + +### 31.1 Overview + +Departments organize faculty, courses, and subjects into academic units (e.g., "Department of Mathematics", "Department of IT"). + +### 31.2 Data Model (OpenEduCat) + +**`op.department`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Department name | +| `code` | Char | Department code | +| `parent_id` | Many2one | Parent department (for hierarchy) | + +### 31.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-DEPT-01 | List departments | `/admin/departments` | Admin | +| FR-DEPT-02 | Create/edit department | `/admin/departments` | Admin | +| FR-DEPT-03 | Delete department | `/admin/departments` | Admin | +| FR-DEPT-04 | Assign department to courses and faculty | Related pages | Admin | + +### 31.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/departments` | List departments | +| `POST` | `/api/departments` | Create department | +| `PATCH` | `/api/departments/{id}` | Update department | +| `DELETE` | `/api/departments/{id}` | Delete department | + +--- + +## 32. Admission and Enrollment + +> **Implementation:** `admission.service.ts` | `encoach_lms_api/controllers/admissions.py` | `AdminAdmissionRegister.tsx`, `AdminAdmissions.tsx` + +### 32.1 Overview + +The admission workflow manages student applications from submission through confirmation to enrollment. Admission registers define open enrollment windows. + +### 32.2 Data Models (OpenEduCat) + +**`op.admission.register`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Register name (e.g., "Fall 2026 Admissions") | +| `course_id` | Many2one | Target course | +| `start_date` | Date | Application window start | +| `end_date` | Date | Application window end | +| `min_count` | Integer | Minimum applications to proceed | +| `max_count` | Integer | Maximum applications accepted | +| `state` | Selection | `draft`, `confirm`, `cancel`, `done` | +| `product_id` | Many2one | Fee product for admission | + +**`op.admission`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Applicant name | +| `application_number` | Char | Auto-generated application number | +| `register_id` | Many2one | Admission register | +| `course_id` | Many2one | Applied course | +| `batch_id` | Many2one | Target batch | +| `first_name` | Char | First name | +| `middle_name` | Char | Middle name | +| `last_name` | Char | Last name | +| `email` | Char | Email | +| `phone` | Char | Phone | +| `birth_date` | Date | Date of birth | +| `gender` | Selection | `m`, `f`, `o` | +| `nationality` | Many2one | Country | +| `state` | Selection | `draft`, `submit`, `confirm`, `admission`, `reject`, `cancel`, `done` | +| `fees` | Float | Admission fee amount | +| `image` | Binary | Applicant photo | +| `student_id` | Many2one | Created student record (after admission) | + +### 32.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-ADM-01 | List admission registers with status | `/admin/admission-register` | Admin | +| FR-ADM-02 | Create/edit admission register (course, dates, capacity) | `/admin/admission-register` | Admin | +| FR-ADM-03 | Open/close admission register | `/admin/admission-register` | Admin | +| FR-ADM-04 | List admissions with filters (status, course, register) | `/admin/admissions` | Admin | +| FR-ADM-05 | View admission detail | `/admin/admissions/:id` | Admin | +| FR-ADM-06 | Transition admission status (submit, confirm, admit, reject) | `/admin/admissions/:id` | Admin | +| FR-ADM-07 | On admit: auto-create `op.student` and `op.student.course` records | Backend | System | +| FR-ADM-08 | Public admission application form | `/apply` | Public/Student | +| FR-ADM-09 | Applicant status tracking | `/apply/status` | Public | + +### 32.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/admission-registers` | List registers | +| `POST` | `/api/admission-registers` | Create register | +| `PATCH` | `/api/admission-registers/{id}` | Update register | +| `POST` | `/api/admission-registers/{id}/confirm` | Confirm register | +| `POST` | `/api/admission-registers/{id}/close` | Close register | +| `GET` | `/api/admissions` | List admissions | +| `GET` | `/api/admissions/{id}` | Get admission detail | +| `POST` | `/api/admissions` | Submit admission application | +| `POST` | `/api/admissions/{id}/submit` | Submit for review | +| `POST` | `/api/admissions/{id}/confirm` | Confirm admission | +| `POST` | `/api/admissions/{id}/admit` | Admit applicant (creates student) | +| `POST` | `/api/admissions/{id}/reject` | Reject admission | + +--- + +## 33. Subject Registration + +> **Implementation:** `institutional-exam.service.ts` | `encoach_lms_api/controllers/subject_registration.py` | `StudentSubjectRegistration.tsx` + +### 33.1 Overview + +Within enrolled courses, students register for specific subjects. This is required for timetable, attendance, and exam eligibility. + +### 33.2 Data Model (OpenEduCat) + +**`op.subject.registration`** + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `course_id` | Many2one | Course | +| `batch_id` | Many2one | Batch | +| `subject_ids` | Many2many | Selected subjects | +| `min_unit_load` | Float | Minimum units from course config | +| `max_unit_load` | Float | Maximum units from course config | +| `state` | Selection | `draft`, `confirm`, `reject`, `done` | + +### 33.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-SR-01 | Students view available subjects for enrolled courses | `/student/subject-registration` | Student | +| FR-SR-02 | Students select subjects within unit load limits | `/student/subject-registration` | Student | +| FR-SR-03 | Submit registration for approval | `/student/subject-registration` | Student | +| FR-SR-04 | Admin/teacher view and approve/reject registrations | `/admin/students` | Admin | +| FR-SR-05 | Confirmed registration updates `op.student.course.subject_ids` | Backend | System | + +### 33.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/subject-registrations` | List registrations (filter by student, course, status) | +| `POST` | `/api/subject-registrations` | Create registration | +| `PATCH` | `/api/subject-registrations/{id}` | Update subjects | +| `POST` | `/api/subject-registrations/{id}/confirm` | Confirm registration | +| `POST` | `/api/subject-registrations/{id}/reject` | Reject registration | +| `GET` | `/api/subject-registrations/available` | Get available subjects for current student | + +--- + +## 34. Institutional Exam Sessions + +> **Implementation:** `institutional-exam.service.ts` | `encoach_lms_api/controllers/inst_exams.py` | `AdminExamSessions.tsx`, `AdminMarksheets.tsx` + +### 34.1 Overview + +Institutional exam sessions group traditional exams (midterms, finals) for a course/batch. These are separate from EnCoach AI-powered exams. They use the OpenEduCat exam, marksheet, and grading system. + +### 34.2 Data Models (OpenEduCat) + +**`op.exam.session`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Session name (e.g., "Fall 2026 Midterms") | +| `course_id` | Many2one | Course | +| `batch_id` | Many2one | Batch | +| `exam_code` | Char | Session code | +| `start_date` | Date | Session start | +| `end_date` | Date | Session end | +| `exam_type` | Many2one | Exam type (midterm, final, quiz) | +| `evaluation_type` | Selection | `normal` or `grade` | +| `venue` | Many2one | Venue partner | +| `state` | Selection | `draft`, `schedule`, `held`, `cancel`, `done` | +| `exam_ids` | One2many | Exams in this session | + +**`op.exam` (institutional)** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Exam name | +| `session_id` | Many2one | Parent session | +| `subject_id` | Many2one | Subject being examined | +| `exam_code` | Char | Exam code | +| `start_time` | Datetime | Exam start | +| `end_time` | Datetime | Exam end | +| `total_marks` | Integer | Maximum marks | +| `min_marks` | Integer | Passing marks | +| `state` | Selection | `draft`, `schedule`, `held`, `result_updated`, `cancel`, `done` | +| `responsible_id` | Many2many | Responsible faculty | +| `attendees_line` | One2many | Exam attendees with marks | + +**`op.exam.type`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Type name (Midterm, Final, Quiz, Lab) | + +**`op.exam.room`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Room name | +| `classroom_id` | Many2one | Linked classroom | +| `capacity` | Integer | Room capacity | + +**`op.exam.attendees`** + +| Field | Type | Description | +|-------|------|-------------| +| `exam_id` | Many2one | Exam | +| `student_id` | Many2one | Student | +| `room_id` | Many2one | Assigned room | +| `marks` | Integer | Obtained marks | +| `status` | Selection | `pass`, `fail` (computed from marks vs min_marks) | + +**`op.marksheet.register`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Register name | +| `exam_session_id` | Many2one | Exam session | +| `marksheet_line` | One2many | Marksheet lines per student | +| `result_template_id` | Many2one | Result template | +| `state` | Selection | `draft`, `validated`, `cancelled` | +| `total_pass` | Integer | Computed pass count | +| `total_failed` | Integer | Computed fail count | + +**`op.marksheet.line`** + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `marksheet_reg_id` | Many2one | Parent register | +| `result_line` | One2many | Per-exam results | +| `total_marks` | Integer | Computed total | +| `percentage` | Float | Computed percentage | +| `grade` | Char | Computed grade (if grade evaluation) | +| `status` | Selection | `pass`, `fail` (computed) | + +**`op.result.line`** + +| Field | Type | Description | +|-------|------|-------------| +| `marksheet_line_id` | Many2one | Parent marksheet line | +| `exam_id` | Many2one | Exam | +| `student_id` | Many2one | Student | +| `marks` | Integer | Marks obtained | +| `grade` | Char | Computed grade | +| `status` | Selection | `pass`, `fail` (computed from marks vs min_marks) | + +**`op.result.template`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Template name | +| `exam_session_id` | Many2one | Exam session | +| `grade_ids` | Many2many | Grade configuration rules | +| `result_date` | Date | Result publication date | +| `state` | Selection | `draft`, `result_generated` | + +**`op.grade.configuration`** + +| Field | Type | Description | +|-------|------|-------------| +| `min_per` | Integer | Minimum percentage | +| `max_per` | Integer | Maximum percentage | +| `result` | Char | Grade display (e.g., "A", "B+", "Pass") | + +### 34.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-IEX-01 | List exam sessions with status filters | `/admin/exam-sessions` | Admin | +| FR-IEX-02 | Create exam session (course, batch, dates, type) | `/admin/exam-sessions` | Admin | +| FR-IEX-03 | Add exams to session (subject, time, marks) | `/admin/exam-sessions` | Admin | +| FR-IEX-04 | Schedule and manage session state transitions | `/admin/exam-sessions` | Admin | +| FR-IEX-05 | Enter marks for exam attendees | `/admin/exam-sessions` | Admin/Teacher | +| FR-IEX-06 | Configure grade scales | `/admin/marksheets` | Admin | +| FR-IEX-07 | Create result templates and generate marksheets | `/admin/marksheets` | Admin | +| FR-IEX-08 | View marksheet results (pass/fail, grades, percentages) | `/admin/marksheets` | Admin | +| FR-IEX-09 | Students view institutional exam results | `/student/grades` | Student | + +### 34.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/inst-exam-sessions` | List exam sessions | +| `POST` | `/api/inst-exam-sessions` | Create session | +| `GET` | `/api/inst-exam-sessions/{id}` | Get session with exams | +| `PATCH` | `/api/inst-exam-sessions/{id}` | Update session | +| `DELETE` | `/api/inst-exam-sessions/{id}` | Delete session | +| `POST` | `/api/inst-exam-sessions/{id}/schedule` | Schedule session | +| `POST` | `/api/inst-exam-sessions/{id}/done` | Mark session done | +| `POST` | `/api/inst-exams` | Add exam to session | +| `PATCH` | `/api/inst-exams/{id}` | Update exam | +| `POST` | `/api/inst-exams/{id}/attendees` | Add attendees | +| `PATCH` | `/api/inst-exams/{id}/marks` | Enter marks | +| `GET` | `/api/exam-types` | List exam types | +| `POST` | `/api/exam-types` | Create exam type | +| `GET` | `/api/grade-config` | List grade configurations | +| `POST` | `/api/grade-config` | Create grade config | +| `GET` | `/api/result-templates` | List result templates | +| `POST` | `/api/result-templates` | Create result template | +| `POST` | `/api/result-templates/{id}/generate` | Generate marksheets | +| `GET` | `/api/marksheets` | List marksheets | +| `GET` | `/api/marksheets/{id}` | Get marksheet with results | +| `POST` | `/api/marksheets/{id}/validate` | Validate marksheet | + +--- + +## 35. Assignment File Submissions + +> **Implementation:** `institutional-exam.service.ts` | `encoach_lms_api/controllers/course_assignments.py` | `TeacherAssignments.tsx`, `StudentAssignments.tsx` + +### 35.1 Overview + +Traditional course assignments where students submit files and teachers grade them. Coexists with EnCoach exam-wrapper assignments (`encoach.assignment`). + +### 35.2 Data Models (OpenEduCat) + +**`grading.assignment.type`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Type name (Homework, Project, Lab Report, Presentation) | + +**`grading.assignment`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Assignment title | +| `course_id` | Many2one | Course | +| `subject_id` | Many2one | Subject | +| `issued_date` | Datetime | Issue date | +| `assignment_type` | Many2one | Assignment type | +| `faculty_id` | Many2one | Issuing faculty | +| `point` | Float | Maximum points | + +**`op.assignment` (extends `grading.assignment`)** + +| Field | Type | Description | +|-------|------|-------------| +| `batch_id` | Many2one | Batch | +| `marks` | Float | Maximum marks | +| `description` | Text | Assignment description | +| `state` | Selection | `draft`, `publish`, `finish`, `cancel` | +| `submission_date` | Datetime | Submission deadline | +| `allocation_ids` | Many2many | Allocated students | +| `assignment_sub_line` | One2many | Student submissions | +| `reviewer` | Many2one | Reviewing faculty | + +**`op.assignment.sub.line`** + +| Field | Type | Description | +|-------|------|-------------| +| `assignment_id` | Many2one | Parent assignment | +| `student_id` | Many2one | Submitting student | +| `description` | Text | Submission notes | +| `submission_date` | Datetime | Actual submission time | +| `attachment_ids` | Many2many | Uploaded files | +| `marks` | Float | Awarded marks | +| `state` | Selection | `draft`, `submit`, `accept`, `reject`, `change_req` | +| `faculty_id` | Many2one | Grading faculty | +| `note` | Text | Faculty feedback | + +### 35.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-SUB-01 | Teacher creates course assignment with description, deadline, files | `/teacher/assignments` | Teacher | +| FR-SUB-02 | Teacher publishes assignment (visible to allocated students) | `/teacher/assignments` | Teacher | +| FR-SUB-03 | Student views assigned course assignments with deadlines | `/student/assignments` | Student | +| FR-SUB-04 | Student uploads files as submission | `/student/assignments` | Student | +| FR-SUB-05 | Teacher views submissions per assignment | `/teacher/assignments/:id` | Teacher | +| FR-SUB-06 | Teacher grades submission (marks, feedback, accept/reject/change request) | `/teacher/assignments/:id` | Teacher | +| FR-SUB-07 | Admin views all course assignments across batches | `/admin/assignments` | Admin | + +### 35.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/course-assignments` | List course assignments | +| `POST` | `/api/course-assignments` | Create assignment | +| `GET` | `/api/course-assignments/{id}` | Get assignment with submissions | +| `PATCH` | `/api/course-assignments/{id}` | Update assignment | +| `POST` | `/api/course-assignments/{id}/publish` | Publish assignment | +| `POST` | `/api/course-assignments/{id}/finish` | Close assignment | +| `GET` | `/api/course-assignments/{id}/submissions` | List submissions | +| `POST` | `/api/course-assignments/{id}/submit` | Student submits (multipart file upload) | +| `PATCH` | `/api/submissions/{id}` | Grade submission (marks, feedback, status) | +| `GET` | `/api/assignment-types` | List assignment types | +| `POST` | `/api/assignment-types` | Create assignment type | + +--- + +# Part VIII-B -- OpenEduCat Enterprise Features + +The following sections cover additional institutional features implemented by the developer beyond the original SRS. These features leverage OpenEduCat Enterprise modules and extend the platform with student lifecycle management, financial tracking, library services, and facility management. + +--- + +## 36. Student Leave Management + +> **Implementation:** `student-leave.service.ts` | `encoach_lms_api/controllers/student_leave.py` | `AdminStudentLeave.tsx` + +### 36.1 Overview + +Students submit leave requests with a type, date range, and justification. Administrators approve or reject requests. Leave types are configurable. + +### 36.2 Data Models + +**`op.student.leave.request`** (OpenEduCat Enterprise) + +| Field | Type | Description | +|-------|------|-------------| +| `request_number` | Char | Auto-generated request number | +| `student_id` | Many2one | Requesting student | +| `leave_type` | Many2one | Leave type reference | +| `start_date` | Date | Leave start | +| `end_date` | Date | Leave end | +| `duration` | Float | Computed duration in days | +| `description` | Text | Justification | +| `state` | Selection | `draft`, `confirm`, `validate`, `approve`, `refuse`, `cancel` | +| `approve_date` | Date | Date of approval | + +**`op.student.leave.type`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Leave type name (e.g., Medical, Personal, Academic) | + +### 36.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-SL-01 | List student leave requests with status filters | `/admin/student-leave` | Admin | +| FR-SL-02 | Create leave request (student, type, dates, description) | `/admin/student-leave` | Admin/Student | +| FR-SL-03 | Approve/reject leave request with state transition | `/admin/student-leave` | Admin | +| FR-SL-04 | Manage leave types (CRUD) | `/admin/student-leave` | Admin | + +### 36.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/student-leaves` | List leave requests | +| `POST` | `/api/student-leaves` | Create leave request | +| `PATCH` | `/api/student-leaves/{id}` | Update leave request | +| `POST` | `/api/student-leaves/{id}/approve` | Approve request | +| `POST` | `/api/student-leaves/{id}/reject` | Reject request | +| `GET` | `/api/student-leave-types` | List leave types | +| `POST` | `/api/student-leave-types` | Create leave type | + +--- + +## 37. Fees Management + +> **Implementation:** `fees.service.ts` | `encoach_lms_api/controllers/fees.py` | `AdminFees.tsx` + +### 37.1 Overview + +Institutional fee plans track student financial obligations per course. The system tracks total, paid, and remaining amounts per student, linked to invoicing. + +### 37.2 Data Models + +**`op.fees.plan`** (OpenEduCat Enterprise) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `course_id` | Many2one | Course | +| `total_amount` | Float | Total fee amount | +| `paid_amount` | Float | Amount paid to date | +| `remaining_amount` | Float | Computed remaining balance | +| `state` | Selection | Fee plan state | + +**`op.student.fees.details`** + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `amount` | Float | Fee amount | +| `date` | Date | Fee date | +| `state` | Selection | Payment state | +| `invoice_state` | Selection | Invoice state | +| `product_name` | Char | Fee product name | + +**`op.fees.terms`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Term name | +| `no_days` | Integer | Number of days for payment | +| `line_ids` | One2many | Fee term lines | + +### 37.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-FEES-01 | List fees plans with student and course details | `/admin/fees` | Admin | +| FR-FEES-02 | View individual student fees with payment history | `/admin/fees` | Admin | +| FR-FEES-03 | View fees terms configuration | `/admin/fees` | Admin | + +### 37.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/fees-plans` | List fee plans | +| `GET` | `/api/fees-plans/{id}` | Get fee plan detail | +| `GET` | `/api/student-fees` | List student fee details | +| `GET` | `/api/fees-terms` | List fees terms | + +--- + +## 38. Lessons Management + +> **Implementation:** `lesson.service.ts` | `encoach_lms_api/controllers/lessons.py` | `AdminLessons.tsx` + +### 38.1 Overview + +Lessons represent individual teaching sessions within timetable sessions. They link subjects to specific timetable slots and track the topic covered. + +### 38.2 Data Model + +**`op.session.lesson`** (OpenEduCat Enterprise) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Lesson name | +| `lesson_topic` | Char | Topic covered | +| `course_id` | Many2one | Course | +| `batch_id` | Many2one | Batch | +| `subject_id` | Many2one | Subject | +| `session_id` | Many2one | Timetable session | +| `faculty_id` | Many2one | Teaching faculty | +| `start_datetime` | Datetime | Lesson start | +| `end_datetime` | Datetime | Lesson end | + +### 38.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-LSN-01 | List lessons with subject and session details | `/admin/lessons` | Admin | +| FR-LSN-02 | Create lesson (name, topic, course, batch, subject, session, faculty, times) | `/admin/lessons` | Admin | +| FR-LSN-03 | Update/delete lessons | `/admin/lessons` | Admin | + +### 38.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/lessons` | List lessons | +| `POST` | `/api/lessons` | Create lesson | +| `PATCH` | `/api/lessons/{id}` | Update lesson | +| `DELETE` | `/api/lessons/{id}` | Delete lesson | + +--- + +## 39. Gradebook and Grading Assignments + +> **Implementation:** `gradebook.service.ts` | `encoach_lms_api/controllers/gradebook.py` | `AdminGradebook.tsx` + +### 39.1 Overview + +The gradebook aggregates student marks across courses and academic years. Grading assignments define assessment items with marks and sequences. Gradebook lines show per-student, per-assignment results with percentages. + +### 39.2 Data Models + +**`op.gradebook`** (OpenEduCat Enterprise) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `course_id` | Many2one | Course | +| `academic_year_id` | Many2one | Academic year | +| `line_ids` | One2many | Gradebook lines | + +**`op.gradebook.line`** + +| Field | Type | Description | +|-------|------|-------------| +| `gradebook_id` | Many2one | Parent gradebook | +| `student_name` | Char | Related student name | +| `assignment_name` | Char | Related assignment name | +| `marks` | Float | Marks obtained | +| `percentage` | Float | Computed percentage | +| `state` | Selection | Line state | + +**`grading.assignment`** (extended) + +| Field | Type | Description | +|-------|------|-------------| +| `sequence` | Char | Assignment sequence code | +| `name` | Char | Assignment name | +| `course_id` | Many2one | Course | +| `subject_id` | Many2one | Subject | +| `state` | Selection | Assignment state | +| `issued_date` | Date | Issue date | + +### 39.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-GB-01 | List gradebooks filtered by course and academic year | `/admin/gradebook` | Admin | +| FR-GB-02 | View gradebook lines per student with marks and percentages | `/admin/gradebook` | Admin | +| FR-GB-03 | Create/update/delete grading assignments | `/admin/gradebook` | Admin | + +### 39.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/gradebooks` | List gradebooks | +| `GET` | `/api/gradebook-lines` | List gradebook lines | +| `GET` | `/api/grading-assignments` | List grading assignments | +| `POST` | `/api/grading-assignments` | Create grading assignment | +| `PATCH` | `/api/grading-assignments/{id}` | Update grading assignment | +| `DELETE` | `/api/grading-assignments/{id}` | Delete grading assignment | + +--- + +## 40. Student Progress Tracking + +> **Implementation:** `student-progress.service.ts` | `encoach_lms_api/controllers/student_progress.py` | `AdminStudentProgress.tsx` + +### 40.1 Overview + +Aggregate view of student academic progression combining attendance, assignment completion, and marksheet results into a unified tracking interface. + +### 40.2 Data Model + +**`op.student.progression`** (OpenEduCat Enterprise) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `total_attendance` | Float | Computed total attendance percentage | +| `total_assignment` | Float | Computed total assignment count/score | +| `total_marksheet_line` | Float | Computed total marksheet entries | + +### 40.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-SP-01 | List student progress with attendance, assignment, and marksheet aggregates | `/admin/student-progress` | Admin | +| FR-SP-02 | Filter by course, batch, or individual student | `/admin/student-progress` | Admin | + +### 40.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/student-progress` | List student progression records | + +--- + +## 41. Library Management + +> **Implementation:** `library.service.ts` | `encoach_lms_api/controllers/library.py` | `AdminLibrary.tsx` + +### 41.1 Overview + +Institutional library management supporting media cataloging, borrowing (movements), and library card issuance. Tracks media items (books, journals, CDs), their circulation, and student/faculty borrowing history. + +### 41.2 Data Models + +**`op.media`** (OpenEduCat Library) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Media title | +| `isbn` | Char | ISBN number | +| `author` | Char | Author name | +| `edition` | Char | Edition | +| `media_type` | Selection | Media type (book, journal, CD, etc.) | + +**`op.media.movement`** + +| Field | Type | Description | +|-------|------|-------------| +| `media_id` | Many2one | Media item | +| `student_id` | Many2one | Borrowing student | +| `faculty_id` | Many2one | Issuing faculty | +| `issued_date` | Date | Issue date | +| `return_date` | Date | Return date | +| `state` | Selection | Movement state (issued, returned, overdue) | + +**`op.library.card`** + +| Field | Type | Description | +|-------|------|-------------| +| `number` | Char | Card number | +| `student_id` | Many2one | Cardholder student | +| `faculty_id` | Many2one | Issuing faculty | + +### 41.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-LIB-01 | Catalog media items with ISBN, author, edition | `/admin/library` | Admin | +| FR-LIB-02 | Track media movements (issue, return) | `/admin/library` | Admin | +| FR-LIB-03 | Issue and manage library cards | `/admin/library` | Admin | +| FR-LIB-04 | Delete media items | `/admin/library` | Admin | + +### 41.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/library/media` | List media items | +| `POST` | `/api/library/media` | Create media item | +| `DELETE` | `/api/library/media/{id}` | Delete media item | +| `GET` | `/api/library/movements` | List movements | +| `POST` | `/api/library/movements` | Create movement (issue) | +| `PATCH` | `/api/library/movements/{id}` | Update movement | +| `POST` | `/api/library/movements/{id}/return` | Return media | +| `GET` | `/api/library/cards` | List library cards | +| `POST` | `/api/library/cards` | Create library card | + +--- + +## 42. Activity Management + +> **Implementation:** `activity.service.ts` | `encoach_lms_api/controllers/activities.py` | `AdminActivities.tsx` + +### 42.1 Overview + +Tracks extracurricular and academic activities per student. Activity types are configurable (sports, cultural, academic, volunteer, etc.). + +### 42.2 Data Models + +**`op.activity`** (OpenEduCat Activity) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `type_id` | Many2one | Activity type | +| `date` | Date | Activity date | +| `description` | Text | Activity description | + +**`op.activity.type`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Type name | + +### 42.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-ACT-01 | List student activities with type and date | `/admin/activities` | Admin | +| FR-ACT-02 | Create/update/delete activities | `/admin/activities` | Admin | +| FR-ACT-03 | Manage activity types (CRUD) | `/admin/activities` | Admin | + +### 42.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/activities` | List activities | +| `POST` | `/api/activities` | Create activity | +| `PATCH` | `/api/activities/{id}` | Update activity | +| `DELETE` | `/api/activities/{id}` | Delete activity | +| `GET` | `/api/activity-types` | List activity types | +| `POST` | `/api/activity-types` | Create activity type | +| `DELETE` | `/api/activity-types/{id}` | Delete activity type | + +--- + +## 43. Facility and Asset Management + +> **Implementation:** `facility.service.ts` | `encoach_lms_api/controllers/facilities.py` | `AdminFacilities.tsx` + +### 43.1 Overview + +Manages institutional facilities (buildings, labs, rooms) and their associated assets (equipment, furniture). Facilities can be linked to classrooms and timetable sessions. + +### 43.2 Data Models + +**`op.facility`** (OpenEduCat Facility) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Facility name | +| `code` | Char | Facility code | + +**`op.facility.line`** (Asset) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Asset name | +| `code` | Char | Asset code | +| `product_name` | Char | Product description | + +### 43.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-FAC-01 | List facilities with codes | `/admin/facilities` | Admin | +| FR-FAC-02 | Create/update/delete facilities | `/admin/facilities` | Admin | +| FR-FAC-03 | List and manage assets within facilities | `/admin/facilities` | Admin | + +### 43.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/facilities` | List facilities | +| `POST` | `/api/facilities` | Create facility | +| `PATCH` | `/api/facilities/{id}` | Update facility | +| `DELETE` | `/api/facilities/{id}` | Delete facility | +| `GET` | `/api/assets` | List assets | +| `POST` | `/api/assets` | Create asset | +| `DELETE` | `/api/assets/{id}` | Delete asset | + +--- + +# Part IX -- Courseware and Content Delivery + +This part covers the Moodle-style structured course delivery system: chapter-based content organization, material management, and AI-powered course content generation. + +--- + +## 44. Course Chapters + +> **Implementation:** `courseware.service.ts` | `encoach_lms_api/controllers/courseware.py` | `TeacherCourseChapters.tsx`, `StudentCourseChapterView.tsx` + +### 44.1 Overview + +Courses are divided into chapters (modules). Teachers create chapters with scheduled start dates, content materials, and lock/unlock controls. Students progress through chapters sequentially or as unlocked by the teacher. + +### 44.2 Data Model (`encoach.course.chapter`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Chapter title | +| `course_id` | Many2one | Parent course (`op.course`) | +| `sequence` | Integer | Display order | +| `description` | Text | Chapter description | +| `start_date` | Datetime | Scheduled start date | +| `end_date` | Datetime | Optional end date | +| `unlock_mode` | Selection | `auto_date` (unlock when start_date reached), `manual` (teacher clicks), `prerequisite` (previous chapter completed) | +| `is_unlocked` | Boolean | Current unlock status | +| `topic_id` | Many2one | Optional link to `encoach.topic` (bridges to adaptive engine) | +| `material_ids` | One2many | Chapter materials | +| `assignment_ids` | Many2many | Linked assignments | +| `exam_ids` | Many2many | Linked exams/quizzes | +| `active` | Boolean | Soft-delete flag | + +### 44.3 Chapter Progress Model (`encoach.chapter.progress`) + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | Many2one | Student | +| `chapter_id` | Many2one | Chapter | +| `status` | Selection | `not_started`, `in_progress`, `completed` | +| `started_at` | Datetime | When student first accessed | +| `completed_at` | Datetime | When marked complete | +| `materials_completed` | Integer | Count of materials viewed/downloaded | +| `materials_total` | Integer | Total materials in chapter | + +### 44.4 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-CH-01 | Teacher creates chapters for a course with name, description, sequence | `/teacher/courses/:id/chapters` | Teacher | +| FR-CH-02 | Teacher sets chapter start date and unlock mode (auto/manual/prerequisite) | `/teacher/courses/:id/chapters` | Teacher | +| FR-CH-03 | Teacher reorders chapters via drag-and-drop | `/teacher/courses/:id/chapters` | Teacher | +| FR-CH-04 | Teacher manually unlocks/locks a chapter | `/teacher/courses/:id/chapters` | Teacher | +| FR-CH-05 | System auto-unlocks chapters when start_date is reached (cron job) | Backend | System | +| FR-CH-06 | Student sees unlocked chapters on course detail page | `/student/courses/:id` | Student | +| FR-CH-07 | Student views chapter content and materials | `/student/courses/:id/chapters/:chapterId` | Student | +| FR-CH-08 | System tracks per-student chapter progress | Backend | System | +| FR-CH-09 | Chapter completion notification sent to student | Backend | System | +| FR-CH-10 | Teacher views chapter progress per student | `/teacher/courses/:id/chapters` | Teacher | + +### 44.5 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/courses/{courseId}/chapters` | List chapters for course | +| `POST` | `/api/courses/{courseId}/chapters` | Create chapter | +| `GET` | `/api/chapters/{id}` | Get chapter detail with materials | +| `PATCH` | `/api/chapters/{id}` | Update chapter | +| `DELETE` | `/api/chapters/{id}` | Delete chapter | +| `POST` | `/api/chapters/{id}/unlock` | Manually unlock chapter | +| `POST` | `/api/chapters/{id}/lock` | Manually lock chapter | +| `PATCH` | `/api/courses/{courseId}/chapters/reorder` | Reorder chapters | +| `GET` | `/api/chapters/{id}/progress` | Get student progress for chapter | +| `POST` | `/api/chapters/{id}/progress/complete` | Mark chapter as completed | + +--- + +## 45. Chapter Materials + +> **Implementation:** `courseware.service.ts` | `encoach_lms_api/controllers/courseware.py` | `TeacherCourseChapters.tsx` + +### 45.1 Overview + +Each chapter contains materials: uploaded files (PDFs, documents), videos, audio, images, hyperlinks, and articles. Teachers control download permissions per material. + +### 45.2 Data Model (`encoach.chapter.material`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Material title | +| `chapter_id` | Many2one | Parent chapter | +| `type` | Selection | `pdf`, `document`, `video`, `audio`, `image`, `link`, `article` | +| `file` | Binary | Uploaded file (for pdf, document, video, audio, image) | +| `url` | Char | External URL (for link, article, or external video) | +| `description` | Text | Material description | +| `sequence` | Integer | Display order | +| `allow_download` | Boolean | Whether students can download the file | +| `is_book` | Boolean | Whether this material is a course book (multi-book support) | +| `book_chapters` | Text (JSON) | Book chapter structure (if following original book chapters) | +| `active` | Boolean | Soft-delete flag | + +### 45.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-MAT-01 | Teacher uploads materials (PDF, Word, video, audio, images) per chapter | `/teacher/courses/:id/chapters/:chapterId` | Teacher | +| FR-MAT-02 | Teacher adds external links, articles, hyperlinks | `/teacher/courses/:id/chapters/:chapterId` | Teacher | +| FR-MAT-03 | Teacher uploads books with chapter structure (follow original or custom) | `/teacher/courses/:id/chapters/:chapterId` | Teacher | +| FR-MAT-04 | Teacher sets download enable/disable per material | `/teacher/courses/:id/chapters/:chapterId` | Teacher | +| FR-MAT-05 | Teacher uploads teaching aids (video, audio, pictures, hyperlinks) | `/teacher/courses/:id/chapters/:chapterId` | Teacher | +| FR-MAT-06 | Student views materials for unlocked chapters | `/student/courses/:id/chapters/:chapterId` | Student | +| FR-MAT-07 | Student downloads materials if download is enabled | `/student/courses/:id/chapters/:chapterId` | Student | +| FR-MAT-08 | System tracks which materials a student has viewed/downloaded | Backend | System | + +### 45.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/chapters/{chapterId}/materials` | List materials | +| `POST` | `/api/chapters/{chapterId}/materials` | Upload material (multipart) | +| `PATCH` | `/api/materials/{id}` | Update material metadata | +| `DELETE` | `/api/materials/{id}` | Delete material | +| `GET` | `/api/materials/{id}/download` | Download material file | +| `POST` | `/api/materials/{id}/viewed` | Mark material as viewed by student | +| `PATCH` | `/api/chapters/{chapterId}/materials/reorder` | Reorder materials | + +--- + +## 46. AI Workbench for Course Content + +> **Implementation:** `courseware.service.ts` | `encoach_lms_api/controllers/courseware.py` | `TeacherAIWorkbench.tsx` + +### 46.1 Overview + +Teachers use the AI Workbench to generate course content automatically. The AI generates chapters, materials, exercises, and grading rubrics based on high-level requirements input by the teacher. + +### 46.2 AI Workbench Flow + +1. Teacher inputs: topic name, objectives, complexity level, target audience +2. AI generates: chapter structure with learning outcomes, reading content, exercises, multimedia suggestions +3. Teacher reviews generated content +4. Teacher can: modify, regenerate specific sections, add additional context +5. Teacher approves and publishes (follows approval workflow if configured) +6. AI can suggest additional materials based on selected student performance profiles + +### 46.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-WB-01 | Teacher opens AI Workbench from course page | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-02 | Teacher inputs high-level requirements (topic, objectives, complexity) | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-03 | AI generates course outline with chapters and learning outcomes | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-04 | AI generates reading content, exercises, and multimedia for each chapter | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-05 | AI generates grading rubrics based on assignment/exam complexity | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-06 | Teacher reviews and modifies AI-generated content | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-07 | Teacher can prompt AI to regenerate specific sections | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-08 | Teacher selects student profiles for AI to suggest additional materials based on performance | `/teacher/courses/:id/workbench` | Teacher | +| FR-WB-09 | Generated content follows approval workflow before publishing | Backend | System | + +### 46.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/api/workbench/generate-outline` | Generate course outline from requirements | +| `POST` | `/api/workbench/generate-chapter` | Generate content for a specific chapter | +| `POST` | `/api/workbench/generate-rubric` | Generate grading rubric | +| `POST` | `/api/workbench/regenerate` | Regenerate a specific section | +| `POST` | `/api/workbench/suggest-materials` | AI suggests materials based on student profiles | +| `POST` | `/api/workbench/publish` | Publish generated content to course chapters | + +--- + +# Part X -- Communication + +## 47. Discussion Boards + +> **Implementation:** `communication.service.ts` | `encoach_lms_api/controllers/communication.py` | `StudentDiscussions.tsx`, `TeacherDiscussions.tsx` + +### 47.1 Overview + +Per-class discussion boards enable student-to-student and student-to-teacher communication. Discussion boards can be attached to courses, chapters, or assignments. + +### 47.2 Data Models + +**`encoach.discussion.board`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Board title | +| `course_id` | Many2one | Associated course | +| `batch_id` | Many2one | Associated batch (optional) | +| `chapter_id` | Many2one | Associated chapter (optional) | +| `assignment_id` | Many2one | Associated assignment (optional) | +| `is_enabled` | Boolean | Whether teacher has enabled discussions | +| `allow_student_posts` | Boolean | Whether students can create new threads | +| `post_count` | Integer | Computed post count | + +**`encoach.discussion.post`** + +| Field | Type | Description | +|-------|------|-------------| +| `board_id` | Many2one | Parent board | +| `parent_id` | Many2one | Parent post (for replies, nested threads) | +| `author_id` | Many2one | Author user | +| `title` | Char | Post title (for root posts) | +| `content` | Text | Post content (supports markdown) | +| `attachment_ids` | Many2many | File attachments | +| `is_pinned` | Boolean | Pinned by teacher | +| `is_resolved` | Boolean | Marked as resolved (for Q&A) | +| `created_at` | Datetime | Post time | + +### 47.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-DISC-01 | Teacher enables/disables discussion board per course or chapter | `/teacher/courses/:id/discussions` | Teacher | +| FR-DISC-02 | Students create discussion threads | `/student/discussions` | Student | +| FR-DISC-03 | Students and teachers reply to threads (nested replies) | `/student/discussions`, `/teacher/courses/:id/discussions` | Both | +| FR-DISC-04 | Teacher pins important posts | `/teacher/courses/:id/discussions` | Teacher | +| FR-DISC-05 | Teacher marks posts as resolved | `/teacher/courses/:id/discussions` | Teacher | +| FR-DISC-06 | Students view other participants within the same class | `/student/discussions` | Student | +| FR-DISC-07 | File attachments on posts | Both pages | Both | + +### 47.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/discussion-boards` | List boards (filter: course_id, batch_id) | +| `POST` | `/api/discussion-boards` | Create board | +| `PATCH` | `/api/discussion-boards/{id}` | Update board settings | +| `GET` | `/api/discussion-boards/{id}/posts` | List posts for board (paginated) | +| `POST` | `/api/discussion-boards/{id}/posts` | Create post/reply | +| `PATCH` | `/api/posts/{id}` | Update post | +| `DELETE` | `/api/posts/{id}` | Delete post | +| `POST` | `/api/posts/{id}/pin` | Pin/unpin post | +| `POST` | `/api/posts/{id}/resolve` | Mark as resolved | + +--- + +## 48. Announcements + +> **Implementation:** `communication.service.ts` | `encoach_lms_api/controllers/communication.py` | `StudentAnnouncements.tsx`, `TeacherAnnouncements.tsx` + +### 48.1 Overview + +Teachers broadcast announcements to classes. System-wide announcements can be created by administrators. + +### 48.2 Data Model (`encoach.announcement`) + +| Field | Type | Description | +|-------|------|-------------| +| `title` | Char | Announcement title | +| `content` | Text | Announcement body (supports markdown) | +| `author_id` | Many2one | Author | +| `course_id` | Many2one | Target course (null = system-wide) | +| `batch_id` | Many2one | Target batch (null = all batches in course) | +| `priority` | Selection | `normal`, `important`, `urgent` | +| `is_published` | Boolean | Published status | +| `published_at` | Datetime | Publish time | +| `expires_at` | Datetime | Expiry time (optional) | +| `send_email` | Boolean | Also send via email | +| `attachment_ids` | Many2many | File attachments | + +### 48.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-ANN-01 | Teacher creates announcement for class/batch | `/teacher/announcements` | Teacher | +| FR-ANN-02 | Teacher sends announcement via platform and/or email | `/teacher/announcements` | Teacher | +| FR-ANN-03 | Admin creates system-wide announcements | `/admin/announcements` | Admin | +| FR-ANN-04 | Students view announcements on dashboard and dedicated page | `/student/announcements` | Student | +| FR-ANN-05 | Priority-based display (urgent announcements highlighted) | All dashboards | All | + +### 48.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/announcements` | List announcements (filter: course_id, priority) | +| `POST` | `/api/announcements` | Create announcement | +| `PATCH` | `/api/announcements/{id}` | Update announcement | +| `DELETE` | `/api/announcements/{id}` | Delete announcement | +| `POST` | `/api/announcements/{id}/publish` | Publish announcement | + +--- + +## 49. Messaging + +> **Implementation:** `communication.service.ts` | `encoach_lms_api/controllers/communication.py` | `StudentMessages.tsx`, `TeacherMessages.tsx` + +### 49.1 Overview + +Direct messaging between platform users. Teachers can message students individually. Students can message other students within the same class (if allowed). Platform can send emails through linked accounts. + +### 49.2 Data Model (`encoach.message`) + +| Field | Type | Description | +|-------|------|-------------| +| `sender_id` | Many2one | Sender user | +| `recipient_id` | Many2one | Recipient user | +| `subject` | Char | Message subject | +| `content` | Text | Message body | +| `is_read` | Boolean | Read status | +| `read_at` | Datetime | When read | +| `attachment_ids` | Many2many | File attachments | +| `send_email_copy` | Boolean | Also send copy via email | +| `created_at` | Datetime | Sent time | + +### 49.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-MSG-01 | Users send direct messages to other users | `/student/messages`, `/teacher/messages` | All | +| FR-MSG-02 | Inbox with read/unread filtering | `/student/messages` | All | +| FR-MSG-03 | Option to send email copy through the platform | Both pages | All | +| FR-MSG-04 | File attachments on messages | Both pages | All | +| FR-MSG-05 | Students see other participants within same class | `/student/messages` | Student | + +### 49.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/messages` | List messages (inbox, filter: unread) | +| `GET` | `/api/messages/sent` | List sent messages | +| `POST` | `/api/messages` | Send message | +| `GET` | `/api/messages/{id}` | Get message detail (marks as read) | +| `DELETE` | `/api/messages/{id}` | Delete message | +| `GET` | `/api/messages/unread-count` | Get unread message count | + +--- + +# Part XI -- Notifications and FAQ + +## 50. Notification Engine + +> **Implementation:** `notification.service.ts` | `encoach_lms_api/controllers/notification.py` | `AdminNotificationRules.tsx` + +### 50.1 Overview + +Centralized notification system supporting both in-app and email notifications. Notifications are triggered by system events (deadlines, chapter unlocks, result releases, announcements) and can be configured by teachers and admins. + +### 50.2 Data Models + +**`encoach.notification`** + +| Field | Type | Description | +|-------|------|-------------| +| `user_id` | Many2one | Recipient user | +| `title` | Char | Notification title | +| `message` | Text | Notification body | +| `type` | Selection | `deadline`, `chapter_unlock`, `result_release`, `announcement`, `assignment`, `exam`, `message`, `system` | +| `action_url` | Char | URL to redirect to when clicked | +| `is_read` | Boolean | Read status | +| `read_at` | Datetime | When read | +| `channel` | Selection | `in_app`, `email`, `both` | +| `created_at` | Datetime | Created time | + +**`encoach.notification.rule`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Rule name | +| `event_type` | Selection | `assignment_due`, `exam_due`, `chapter_unlock`, `result_release`, `submission_graded`, `extension_response` | +| `days_before` | Integer | Days before event to send notification | +| `frequency` | Selection | `once`, `daily`, `custom` | +| `custom_intervals` | Text (JSON) | Custom reminder intervals (e.g., [7, 3, 1] days before) | +| `channel` | Selection | `in_app`, `email`, `both` | +| `entity_id` | Many2one | Entity scope | +| `active` | Boolean | Active flag | + +**`encoach.notification.preferences`** + +| Field | Type | Description | +|-------|------|-------------| +| `user_id` | Many2one | User | +| `email_enabled` | Boolean | Receive email notifications | +| `assignment_alerts` | Boolean | Assignment deadline alerts | +| `exam_alerts` | Boolean | Exam alerts | +| `chapter_alerts` | Boolean | Chapter unlock alerts | +| `announcement_alerts` | Boolean | Announcement alerts | +| `message_alerts` | Boolean | New message alerts | + +### 50.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-NOT-01 | In-app notification bell with unread count (existing NotificationDropdown enhanced) | All layouts | All | +| FR-NOT-02 | Email notifications for configured events | Backend | System | +| FR-NOT-03 | Admin configures notification rules (event type, timing, frequency) | `/admin/notification-rules` | Admin | +| FR-NOT-04 | Teacher sets reminder frequency for assignments and exams | Assignment/exam creation forms | Teacher | +| FR-NOT-05 | Students receive deadline alerts, chapter unlock alerts, result release alerts | Dashboard | Student | +| FR-NOT-06 | Users configure personal notification preferences | Profile pages | All | +| FR-NOT-07 | Notification action button redirects to relevant page | All layouts | All | + +### 50.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/notifications` | List notifications (paginated, filter: type, is_read) | +| `POST` | `/api/notifications/{id}/read` | Mark as read | +| `POST` | `/api/notifications/read-all` | Mark all as read | +| `GET` | `/api/notifications/unread-count` | Get unread count | +| `GET` | `/api/notification-rules` | List rules | +| `POST` | `/api/notification-rules` | Create rule | +| `PATCH` | `/api/notification-rules/{id}` | Update rule | +| `DELETE` | `/api/notification-rules/{id}` | Delete rule | +| `GET` | `/api/notification-preferences` | Get user preferences | +| `PATCH` | `/api/notification-preferences` | Update user preferences | + +--- + +## 51. FAQ System + +> **Implementation:** `faq.service.ts` | `encoach_lms_api/controllers/faq.py` | `AdminFAQ.tsx`, `FAQ.tsx` (public) + +### 51.1 Overview + +Interactive FAQ system editable by super admin. FAQ items are organized by categories and filtered by target audience (student, entity, or both). Supports text, images, and video content. + +### 51.2 Data Models + +**`encoach.faq.category`** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Category name | +| `sequence` | Integer | Display order | +| `icon` | Char | Icon identifier | +| `audience` | Selection | `student`, `entity`, `both` | +| `active` | Boolean | Active flag | + +**`encoach.faq.item`** + +| Field | Type | Description | +|-------|------|-------------| +| `question` | Char | Question text | +| `answer` | Text | Answer (supports markdown, embedded images/video) | +| `category_id` | Many2one | Parent category | +| `audience` | Selection | `student`, `entity`, `both` | +| `image` | Binary | Optional image | +| `video_url` | Char | Optional video URL | +| `sequence` | Integer | Display order | +| `active` | Boolean | Active flag | + +### 51.3 Functional Requirements + +| ID | Requirement | Route | Role | +|----|-------------|-------|------| +| FR-FAQ-01 | Super admin manages FAQ categories and items | `/admin/faq` | Admin | +| FR-FAQ-02 | Admin adds text, images, and video to FAQ answers | `/admin/faq` | Admin | +| FR-FAQ-03 | Admin sets audience filter per category/item (student, entity, both) | `/admin/faq` | Admin | +| FR-FAQ-04 | Students and entity users view FAQs filtered by their role | `/faq` | All | +| FR-FAQ-05 | Search bar to find FAQ items by keyword | `/faq` | All | +| FR-FAQ-06 | Interactive accordion-style display (click question to expand answer) | `/faq` | All | + +### 51.4 API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/faq/categories` | List categories (filter: audience) | +| `POST` | `/api/faq/categories` | Create category | +| `PATCH` | `/api/faq/categories/{id}` | Update category | +| `DELETE` | `/api/faq/categories/{id}` | Delete category | +| `GET` | `/api/faq/items` | List items (filter: category_id, audience, search) | +| `POST` | `/api/faq/items` | Create item | +| `PATCH` | `/api/faq/items/{id}` | Update item | +| `DELETE` | `/api/faq/items/{id}` | Delete item | + +--- + +# Part XII -- Technical Specifications + +## 52. Data Models + +### 52.1 Existing Models (from `ielts-be-v0`) + +| Model | Module | Purpose | +|-------|--------|---------| +| `res.users` (extended) | `encoach_core` | User accounts with EnCoach fields | +| `encoach.entity` | `encoach_core` | Tenant organizations | +| `encoach.role` | `encoach_core` | Entity roles | +| `encoach.permission` | `encoach_core` | Role permissions | +| `encoach.code` | `encoach_core` | Registration codes | +| `encoach.invite` | `encoach_core` | User invitations | +| `encoach.exam` | `encoach_exam` | Exam definitions (extended with `subject_id`, `topic_ids`) | +| `encoach.approval.workflow` | `encoach_exam` | Exam approval workflows | +| `encoach.assignment` | `encoach_assignment` | Exam assignments | +| `encoach.group` | `encoach_classroom` | Classroom groups | +| `encoach.session` | `encoach_stats` | Exam session state | +| `encoach.stat` | `encoach_stats` | Per-exercise statistics (extended with `topic_id`, `subject_id`) | +| `encoach.evaluation` | `encoach_evaluation` | Writing/speaking evaluation results | +| `encoach.ai.job` | `encoach_evaluation` | Background AI job tracking | +| `encoach.training` | `encoach_training` | Training content records | +| `encoach.training.tip` | `encoach_training` | Training tips with FAISS embeddings | +| `encoach.walkthrough` | `encoach_training` | Module walkthroughs | +| `encoach.package` | `encoach_subscription` | Subscription packages | +| `encoach.subscription.payment` | `encoach_subscription` | Payment records | +| `encoach.discount` | `encoach_subscription` | Discount codes | +| `encoach.ticket` | `encoach_ticket` | Support tickets | +| `encoach.elai.avatar` | `encoach_ai_media` | ELAI avatar profiles | +| `encoach.registration` | `encoach_registration` | Batch registration mixin | + +### 52.2 New Models (Adaptive Learning Engine) + +| Model | Module | Purpose | Key Fields | +|-------|--------|---------|------------| +| `encoach.subject` | `encoach_taxonomy` | Top-level subjects | `name`, `code`, `is_active`, `diagnostic_config` (JSON), `mastery_threshold`, `grading_scale`, `grading_scale_config` (JSON) | +| `encoach.domain` | `encoach_taxonomy` | Subject domains | `name`, `code`, `subject_id`, `sequence` | +| `encoach.topic` | `encoach_taxonomy` | Topics with prerequisites | `name`, `code`, `domain_id`, `estimated_hours`, `difficulty_level`, `prerequisite_ids` (M2M self-ref), `question_type_weights` (JSON) | +| `encoach.learning.objective` | `encoach_taxonomy` | Measurable objectives | `name`, `topic_id`, `bloom_level`, `sequence` | +| `encoach.resource` | `encoach_resources` | Human-uploaded materials | `name`, `resource_type`, `file`/`url`, `topic_ids` (M2M), `author_id`, `is_published`, `review_status` | +| `encoach.proficiency` | `encoach_adaptive` | Per-student per-topic mastery | `student_id`, `topic_id`, `mastery` (float 0-100), `mastery_level`, `last_assessed`, `time_spent_minutes` | +| `encoach.learning.plan` | `encoach_adaptive` | Personalized study plan | `student_id`, `subject_id`, `status`, `ai_summary`, `overall_progress`, `target_completion` | +| `encoach.learning.plan.item` | `encoach_adaptive` | Plan items | `plan_id`, `topic_id`, `sequence`, `status` (locked/available/in_progress/completed), `estimated_hours`, `actual_hours` | +| `encoach.resource.completion` | `encoach_adaptive` | Resource viewing tracking | `student_id`, `resource_id`, `viewed`, `time_spent_minutes`, `rating` | +| `encoach.ai.content.cache` | `encoach_adaptive` | Cached AI-generated content | `topic_id`, `content_type`, `difficulty_level`, `content`, `model_used`, `review_status` | + +### 52.3 New Models (SIS Integration) + +| Model | Module | Purpose | +|-------|--------|---------| +| `encoach.sis.sync` | `encoach_sis` | SIS sync job tracking | +| `encoach.sis.mapping` | `encoach_sis` | Field mapping between SIS and EnCoach | + +### 52.4 New Models (Branding) + +| Model | Module | Purpose | +|-------|--------|---------| +| `encoach.branding` | `encoach_branding` | Tenant branding configuration (logo, colors, fonts) | + +### 52.5 New Models (OpenEduCat Enterprise -- Beyond SRS) + +| Model | Module | Purpose | Key Fields | +|-------|--------|---------|------------| +| `op.student.leave.request` | `openeducat_student_leave_enterprise` | Student leave requests | `request_number`, `student_id`, `leave_type`, `start_date`, `end_date`, `duration`, `description`, `state`, `approve_date` | +| `op.student.leave.type` | `openeducat_student_leave_enterprise` | Leave type definitions | `name` | +| `op.fees.plan` | `openeducat_fees` | Student fee plans | `student_id`, `course_id`, `total_amount`, `paid_amount`, `remaining_amount`, `state` | +| `op.student.fees.details` | `openeducat_fees` | Fee payment details | `student_id`, `amount`, `date`, `state`, `invoice_state`, `product_name` | +| `op.fees.terms` | `openeducat_fees` | Fee payment terms | `name`, `no_days`, `line_ids` | +| `op.session.lesson` | `openeducat_lesson` | Lesson sessions | `name`, `lesson_topic`, `course_id`, `batch_id`, `subject_id`, `session_id`, `faculty_id`, `start_datetime`, `end_datetime` | +| `op.gradebook` | `openeducat_grading` | Student gradebooks | `student_id`, `course_id`, `academic_year_id`, `line_ids` | +| `op.gradebook.line` | `openeducat_grading` | Gradebook entries | `gradebook_id`, `marks`, `percentage`, `state` | +| `op.student.progression` | `openeducat_student_progress_enterprise` | Aggregate progress tracking | `student_id`, `total_attendance`, `total_assignment`, `total_marksheet_line` | +| `op.media` | `openeducat_library` | Library media catalog | `name`, `isbn`, `author`, `edition`, `media_type` | +| `op.media.movement` | `openeducat_library` | Library circulation | `media_id`, `student_id`, `faculty_id`, `issued_date`, `return_date`, `state` | +| `op.library.card` | `openeducat_library` | Library cards | `number`, `student_id`, `faculty_id` | +| `op.activity` | `openeducat_activity` | Student activities | `student_id`, `type_id`, `date`, `description` | +| `op.activity.type` | `openeducat_activity` | Activity type definitions | `name` | +| `op.facility` | `openeducat_facility` | Institutional facilities | `name`, `code` | +| `op.facility.line` | `openeducat_facility` | Facility assets | `name`, `code`, `product_name` | + +### 52.6 Frontend Type Definitions (29 files) + +The frontend defines TypeScript interfaces matching all backend models: + +| Type File | Key Interfaces | +|-----------|---------------| +| `common.ts` | `PaginatedResponse`, `ApiResponse`, `PaginationParams` | +| `auth.ts` | `User`, `UserRole`, `LoginRequest`, `LoginResponse` | +| `entity.ts` | `Entity`, `EntityRole`, `RolePermission` | +| `exam.ts` | `Exam`, `Rubric`, `ExamStructure` | +| `assignment.ts` | `Assignment`, `AssignmentCreateRequest` | +| `classroom.ts` | `Classroom`, `ClassroomMember` | +| `stats.ts` | `ExamSession`, `Stat`, `Evaluation` | +| `subscription.ts` | `Package`, `Payment`, `Discount` | +| `ticket.ts` | `Ticket` | +| `training.ts` | `Training`, `Tip`, `Walkthrough` | +| `taxonomy.ts` | `Subject`, `Domain`, `Topic`, `LearningObjective`, `TaxonomyTree` | +| `adaptive.ts` | `Proficiency`, `LearningPlan`, `DiagnosticSession`, `TopicContent` | +| `lms.ts` | `Course`, `Batch`, `Timetable`, `Attendance`, `Grade` | +| `ai.ts` | `Chat`, `Insight`, `Alert`, `AiGeneration`, `AiGrading` | +| `academic.ts` | `AcademicYear`, `Term`, `Department` | +| `admission.ts` | `Register`, `Admission` | +| `institutional-exam.ts` | `ExamSession`, `Marksheet`, `Submission` | +| `courseware.ts` | `Chapter`, `Material`, `Workbench` | +| `communication.ts` | `DiscussionBoard`, `Post`, `Announcement`, `Message` | +| `notification.ts` | `Notification`, `NotificationRule`, `Preferences` | +| `faq.ts` | `FaqCategory`, `FaqItem` | +| `student-leave.ts` | `StudentLeaveRequest`, `StudentLeaveType` | +| `fees.ts` | `FeesPlan`, `StudentFeesDetail`, `FeesTerms` | +| `lesson.ts` | `Lesson`, `LessonCreateRequest` | +| `gradebook.ts` | `Gradebook`, `GradebookLine`, `GradingAssignment` | +| `student-progress.ts` | `StudentProgression` | +| `library.ts` | `MediaItem`, `MediaMovement`, `LibraryCard` | +| `activity.ts` | `Activity`, `ActivityType` | +| `facility.ts` | `Facility`, `Asset` | + +--- + +## 53. REST API Specification + +### 53.1 Existing Endpoints (81+) + +All existing endpoints from the Odoo 19 backend remain. Key groups: + +| Group | Endpoints | Count | +|-------|-----------|-------| +| Auth | `/api/login`, `/api/logout`, `/api/reset`, `/api/reset/sendVerification` | 4 | +| Users | `/api/user`, `/api/users/update`, `/api/users/list`, `/api/users/{id}` | 6 | +| Entities & Roles | `/api/entities`, `/api/roles`, `/api/permissions` | 7 | +| Exams | `/api/exam`, `/api/exam/{module}`, `/api/exam/{module}/{id}`, `/api/exam/avatars` | 7 | +| AI Generation | `/api/exam/reading/*`, `/api/exam/listening/*`, `/api/exam/writing/*`, `/api/exam/speaking/*`, `/api/exam/level/*` | 12 | +| Media | `/api/exam/listening/media`, `/api/exam/speaking/media`, `/api/transcribe` | 6 | +| Evaluations | `/api/evaluate/writing`, `/api/evaluate/speaking`, `/api/grading/multiple` | 6 | +| Classrooms | `/api/groups` | 3 | +| Assignments | `/api/assignments` | 8 | +| Sessions & Stats | `/api/sessions`, `/api/stats`, `/api/statistical` | 5 | +| Training | `/api/training`, `/api/training/tips`, `/api/training/walkthrough` | 4 | +| Subscriptions | `/api/packages`, `/api/stripe`, `/api/paypal`, `/api/paymob` + webhooks | 7 | +| Registration | `/api/register`, `/api/code/{code}`, `/api/batch_users` | 4 | +| Tickets | `/api/tickets` | 3 | +| Storage | `/api/storage` | 2 | +| Approvals | `/api/approval-workflows` | 3 | + +### 53.2 New Endpoints (Adaptive Learning Engine, 40+) + +| Group | Method | Path | Description | +|-------|--------|------|-------------| +| **Taxonomy** | `GET` | `/api/subjects` | List subjects | +| | `GET` | `/api/subjects/{id}` | Get subject detail | +| | `POST` | `/api/subjects` | Create subject | +| | `PATCH` | `/api/subjects/{id}` | Update subject | +| | `DELETE` | `/api/subjects/{id}` | Delete subject | +| | `GET` | `/api/subjects/{id}/taxonomy` | Full taxonomy tree | +| | `POST` | `/api/subjects/{id}/taxonomy/import` | Bulk import | +| | `GET/POST/PATCH/DELETE` | `/api/domains`, `/api/topics` | Domain and topic CRUD | +| **Resources** | `GET` | `/api/resources` | List (filter by topic, type) | +| | `POST` | `/api/resources` | Upload resource (multipart) | +| | `PATCH` | `/api/resources/{id}` | Update metadata | +| | `DELETE` | `/api/resources/{id}` | Delete | +| | `GET` | `/api/resources/{id}/download` | Download file | +| | `POST` | `/api/resources/{id}/complete` | Mark viewed | +| | `POST` | `/api/resources/{id}/rate` | Rate (1-5) | +| **Diagnostic** | `POST` | `/api/diagnostic/start` | Start adaptive diagnostic | +| | `POST` | `/api/diagnostic/answer` | Submit answer, get next question | +| | `GET` | `/api/diagnostic/{id}/result` | Get diagnostic results | +| **Proficiency** | `GET` | `/api/proficiency` | Student's profile (by subject) | +| | `GET` | `/api/proficiency/summary` | Cross-subject summary | +| | `GET` | `/api/proficiency/class` | Class-level (teacher/admin) | +| **Learning Plan** | `GET` | `/api/learning-plan` | Active plan (by subject) | +| | `POST` | `/api/learning-plan/generate` | Generate/regenerate | +| | `PATCH` | `/api/learning-plan/{id}` | Teacher override | +| | `POST` | `/api/learning-plan/{id}/pause` | Pause plan | +| | `POST` | `/api/learning-plan/{id}/resume` | Resume plan | +| **Content** | `GET` | `/api/topics/{id}/content` | Get learning content (hybrid) | +| | `POST` | `/api/topics/{id}/generate-content` | Force AI generation | +| | `POST` | `/api/topics/{id}/practice` | Generate practice questions | +| | `POST` | `/api/topics/{id}/practice/grade` | Grade practice answers | +| | `POST` | `/api/topics/{id}/mastery-quiz` | Start mastery quiz | +| | `POST` | `/api/topics/{id}/mastery-quiz/submit` | Submit mastery quiz | +| **AI Coaching** | `POST` | `/api/coach/chat` | AI assistant conversation | +| | `POST` | `/api/coach/hint` | Request hint | +| | `POST` | `/api/coach/explain` | Request explanation | +| | `POST` | `/api/coach/suggest` | Study suggestions | +| | `POST` | `/api/coach/writing-help` | Writing assistance | +| | `GET` | `/api/coach/tip` | Contextual tip | +| **AI Utilities** | `POST` | `/api/ai/search` | Semantic search | +| | `POST` | `/api/ai/insights` | Data insights | +| | `GET` | `/api/ai/alerts` | Risk alerts | +| | `POST` | `/api/ai/report-narrative` | Report narrative | +| | `POST` | `/api/ai/batch-optimize` | Batch optimization | +| **Analytics** | `GET` | `/api/analytics/student` | Student analytics | +| | `GET` | `/api/analytics/class` | Class analytics | +| | `GET` | `/api/analytics/subject` | Subject analytics | +| | `GET` | `/api/analytics/content-gaps` | Content gap report | + +### 53.3 New Endpoints (LMS API Bridge) + +| Method | Path | Description | +|--------|------|-------------| +| `GET/POST` | `/api/courses` | List/create courses | +| `GET/PATCH/DELETE` | `/api/courses/{id}` | Course CRUD | +| `POST` | `/api/courses/ai-generate` | AI-generate course outline | +| `GET/POST` | `/api/batches` | List/create batches | +| `GET/PATCH/DELETE` | `/api/batches/{id}` | Batch CRUD | +| `GET/POST` | `/api/timetable` | Timetable sessions | +| `GET/POST` | `/api/attendance` | Attendance records | +| `GET` | `/api/grades` | Gradebook | + +### 53.4 New Endpoints (SIS Integration) + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/api/sis/sync` | Trigger SIS sync | +| `GET` | `/api/sis/status` | Sync status | +| `GET` | `/api/sis/mappings` | Field mappings | +| `PATCH` | `/api/sis/mappings` | Update mappings | + +### 53.5 New Endpoints (OpenEduCat Institutional LMS, 60+) + +| Group | Method | Path | Description | +|-------|--------|------|-------------| +| **Academic Years** | `GET/POST` | `/api/academic-years` | List/create years | +| | `GET/PATCH/DELETE` | `/api/academic-years/{id}` | Year CRUD | +| | `POST` | `/api/academic-years/{id}/generate-terms` | Auto-generate terms | +| | `GET/POST` | `/api/academic-terms` | List/create terms | +| | `PATCH/DELETE` | `/api/academic-terms/{id}` | Term CRUD | +| **Departments** | `GET/POST` | `/api/departments` | List/create | +| | `PATCH/DELETE` | `/api/departments/{id}` | CRUD | +| **Admissions** | `GET/POST` | `/api/admission-registers` | List/create registers | +| | `PATCH` | `/api/admission-registers/{id}` | Update | +| | `POST` | `/api/admission-registers/{id}/confirm` | Confirm register | +| | `POST` | `/api/admission-registers/{id}/close` | Close register | +| | `GET/POST` | `/api/admissions` | List/create admissions | +| | `GET` | `/api/admissions/{id}` | Admission detail | +| | `POST` | `/api/admissions/{id}/submit` | Submit for review | +| | `POST` | `/api/admissions/{id}/confirm` | Confirm | +| | `POST` | `/api/admissions/{id}/admit` | Admit (creates student) | +| | `POST` | `/api/admissions/{id}/reject` | Reject | +| **Subject Registration** | `GET/POST` | `/api/subject-registrations` | List/create | +| | `PATCH` | `/api/subject-registrations/{id}` | Update subjects | +| | `POST` | `/api/subject-registrations/{id}/confirm` | Confirm | +| | `POST` | `/api/subject-registrations/{id}/reject` | Reject | +| | `GET` | `/api/subject-registrations/available` | Available subjects | +| **Inst. Exam Sessions** | `GET/POST` | `/api/inst-exam-sessions` | List/create | +| | `GET/PATCH/DELETE` | `/api/inst-exam-sessions/{id}` | CRUD | +| | `POST` | `/api/inst-exam-sessions/{id}/schedule` | Schedule | +| | `POST` | `/api/inst-exam-sessions/{id}/done` | Mark done | +| | `POST` | `/api/inst-exams` | Add exam | +| | `PATCH` | `/api/inst-exams/{id}` | Update exam | +| | `POST` | `/api/inst-exams/{id}/attendees` | Add attendees | +| | `PATCH` | `/api/inst-exams/{id}/marks` | Enter marks | +| | `GET/POST` | `/api/exam-types` | List/create types | +| | `GET/POST` | `/api/grade-config` | Grade configuration | +| | `GET/POST` | `/api/result-templates` | Result templates | +| | `POST` | `/api/result-templates/{id}/generate` | Generate marksheets | +| | `GET` | `/api/marksheets` | List marksheets | +| | `GET` | `/api/marksheets/{id}` | Marksheet detail | +| | `POST` | `/api/marksheets/{id}/validate` | Validate | +| **Course Assignments** | `GET/POST` | `/api/course-assignments` | List/create | +| | `GET/PATCH` | `/api/course-assignments/{id}` | CRUD | +| | `POST` | `/api/course-assignments/{id}/publish` | Publish | +| | `POST` | `/api/course-assignments/{id}/finish` | Close | +| | `GET` | `/api/course-assignments/{id}/submissions` | List submissions | +| | `POST` | `/api/course-assignments/{id}/submit` | Student submits | +| | `PATCH` | `/api/submissions/{id}` | Grade submission | +| | `GET/POST` | `/api/assignment-types` | Assignment types | + +--- + +### 53.6 New Endpoints (Courseware, Communication, Notifications, FAQ, 60+) + +| Group | Method | Path | Description | +|-------|--------|------|-------------| +| **Chapters** | `GET` | `/api/courses/{courseId}/chapters` | List chapters | +| | `POST` | `/api/courses/{courseId}/chapters` | Create chapter | +| | `GET` | `/api/chapters/{id}` | Chapter detail | +| | `PATCH` | `/api/chapters/{id}` | Update chapter | +| | `DELETE` | `/api/chapters/{id}` | Delete chapter | +| | `POST` | `/api/chapters/{id}/unlock` | Unlock chapter | +| | `POST` | `/api/chapters/{id}/lock` | Lock chapter | +| | `PATCH` | `/api/courses/{courseId}/chapters/reorder` | Reorder | +| | `GET` | `/api/chapters/{id}/progress` | Progress | +| | `POST` | `/api/chapters/{id}/progress/complete` | Complete | +| **Materials** | `GET` | `/api/chapters/{chapterId}/materials` | List | +| | `POST` | `/api/chapters/{chapterId}/materials` | Upload | +| | `PATCH` | `/api/materials/{id}` | Update | +| | `DELETE` | `/api/materials/{id}` | Delete | +| | `GET` | `/api/materials/{id}/download` | Download | +| | `POST` | `/api/materials/{id}/viewed` | Mark viewed | +| **Workbench** | `POST` | `/api/workbench/generate-outline` | Generate outline | +| | `POST` | `/api/workbench/generate-chapter` | Generate chapter | +| | `POST` | `/api/workbench/generate-rubric` | Generate rubric | +| | `POST` | `/api/workbench/regenerate` | Regenerate section | +| | `POST` | `/api/workbench/suggest-materials` | Suggest materials | +| | `POST` | `/api/workbench/publish` | Publish content | +| **Discussions** | `GET/POST` | `/api/discussion-boards` | List/create boards | +| | `PATCH` | `/api/discussion-boards/{id}` | Update board | +| | `GET/POST` | `/api/discussion-boards/{id}/posts` | List/create posts | +| | `PATCH/DELETE` | `/api/posts/{id}` | Update/delete post | +| | `POST` | `/api/posts/{id}/pin` | Pin post | +| | `POST` | `/api/posts/{id}/resolve` | Resolve post | +| **Announcements** | `GET/POST` | `/api/announcements` | List/create | +| | `PATCH/DELETE` | `/api/announcements/{id}` | Update/delete | +| | `POST` | `/api/announcements/{id}/publish` | Publish | +| **Messages** | `GET` | `/api/messages` | Inbox | +| | `GET` | `/api/messages/sent` | Sent | +| | `POST` | `/api/messages` | Send | +| | `GET` | `/api/messages/{id}` | Detail | +| | `DELETE` | `/api/messages/{id}` | Delete | +| | `GET` | `/api/messages/unread-count` | Unread count | +| **Notifications** | `GET` | `/api/notifications` | List | +| | `POST` | `/api/notifications/{id}/read` | Mark read | +| | `POST` | `/api/notifications/read-all` | Read all | +| | `GET` | `/api/notifications/unread-count` | Count | +| | `GET/POST` | `/api/notification-rules` | Rules | +| | `PATCH/DELETE` | `/api/notification-rules/{id}` | Rule CRUD | +| | `GET/PATCH` | `/api/notification-preferences` | User prefs | +| **FAQ** | `GET/POST` | `/api/faq/categories` | Categories | +| | `PATCH/DELETE` | `/api/faq/categories/{id}` | Category CRUD | +| | `GET/POST` | `/api/faq/items` | Items | +| | `PATCH/DELETE` | `/api/faq/items/{id}` | Item CRUD | +| **Plagiarism** | `POST` | `/api/plagiarism/check` | Check text | +| | `GET` | `/api/plagiarism/report/{submissionId}` | Get report | +| | `GET` | `/api/plagiarism/report/{submissionId}/download` | Download PDF | +| **Exam Access** | `POST` | `/api/exam/{id}/generate-link` | Generate access link | +| | `GET` | `/api/exam/access/{token}` | Access via link | +| | `POST` | `/api/exam/official-login` | Official exam login | + +### 53.7 New Endpoints (OpenEduCat Enterprise Features, 40+) + +| Group | Method | Path | Description | +|-------|--------|------|-------------| +| **Student Leave** | `GET` | `/api/student-leaves` | List leave requests | +| | `POST` | `/api/student-leaves` | Create leave request | +| | `PATCH` | `/api/student-leaves/{id}` | Update leave | +| | `POST` | `/api/student-leaves/{id}/approve` | Approve | +| | `POST` | `/api/student-leaves/{id}/reject` | Reject | +| | `GET/POST` | `/api/student-leave-types` | Leave types | +| **Fees** | `GET` | `/api/fees-plans` | List fee plans | +| | `GET` | `/api/fees-plans/{id}` | Fee plan detail | +| | `GET` | `/api/student-fees` | Student fee details | +| | `GET` | `/api/fees-terms` | Fee terms | +| **Lessons** | `GET/POST` | `/api/lessons` | List/create lessons | +| | `PATCH/DELETE` | `/api/lessons/{id}` | Update/delete | +| **Gradebook** | `GET` | `/api/gradebooks` | List gradebooks | +| | `GET` | `/api/gradebook-lines` | Gradebook lines | +| | `GET/POST` | `/api/grading-assignments` | Grading assignments | +| | `PATCH/DELETE` | `/api/grading-assignments/{id}` | CRUD | +| **Student Progress** | `GET` | `/api/student-progress` | Progression records | +| **Library** | `GET/POST` | `/api/library/media` | Media items | +| | `DELETE` | `/api/library/media/{id}` | Delete media | +| | `GET/POST` | `/api/library/movements` | Movements | +| | `PATCH` | `/api/library/movements/{id}` | Update movement | +| | `POST` | `/api/library/movements/{id}/return` | Return media | +| | `GET/POST` | `/api/library/cards` | Library cards | +| **Activities** | `GET/POST` | `/api/activities` | Activities | +| | `PATCH/DELETE` | `/api/activities/{id}` | CRUD | +| | `GET/POST` | `/api/activity-types` | Activity types | +| | `DELETE` | `/api/activity-types/{id}` | Delete type | +| **Facilities** | `GET/POST` | `/api/facilities` | Facilities | +| | `PATCH/DELETE` | `/api/facilities/{id}` | CRUD | +| | `GET/POST` | `/api/assets` | Assets | +| | `DELETE` | `/api/assets/{id}` | Delete asset | +| **Roles** | `GET/POST` | `/api/roles` | Roles | +| | `GET/PATCH/DELETE` | `/api/roles/{id}` | Role CRUD | +| | `PATCH` | `/api/roles/{id}/permissions` | Update permissions | +| | `GET/POST` | `/api/permissions` | Permissions | +| | `DELETE` | `/api/permissions/{id}` | Delete permission | +| | `GET` | `/api/users/with-roles` | Users with roles | +| | `GET/PATCH` | `/api/users/{id}/roles` | User roles | +| | `POST` | `/api/users/{id}/roles/toggle` | Toggle role | +| | `GET` | `/api/authority-matrix` | Authority matrix | +| | `POST` | `/api/authority-matrix/toggle` | Toggle cell | +| | `GET` | `/api/user-authority-matrix` | User authority | + +--- + +## 54. Frontend Page Inventory + +### 54.1 Auth Pages (3) + +| Route | Page | API Dependencies | +|-------|------|-----------------| +| `/login` | Login | `POST /api/login` | +| `/register` | Register | `POST /api/register` | +| `/forgot-password` | Forgot Password | `POST /api/reset/sendVerification` | + +### 54.2 Student Pages (18+) + +| Route | Page | Key APIs | AI Components | +|-------|------|----------|---------------| +| `/student/dashboard` | Student Dashboard | `/api/user`, `/api/proficiency/summary`, `/api/learning-plan` | AiStudyCoach, AiTipBanner | +| `/student/subjects` | **NEW:** Subject Selection | `/api/subjects`, `/api/proficiency/summary` | -- | +| `/student/diagnostic/:subjectId` | **NEW:** Diagnostic Test | `/api/diagnostic/start`, `/api/diagnostic/answer` | -- | +| `/student/proficiency/:subjectId` | **NEW:** Proficiency Profile | `/api/proficiency?subjectId=` | -- | +| `/student/plan/:subjectId` | **NEW:** Learning Plan | `/api/learning-plan?subjectId=` | AiStudyCoach | +| `/student/topic/:topicId` | **NEW:** Topic Learning | `/api/topics/{id}/content`, `/api/resources` | AiCoach (hints, explanations) | +| `/student/courses` | Course List | `/api/courses` | AiTipBanner | +| `/student/courses/:id` | Course Detail | `/api/courses/{id}` | -- | +| `/student/assignments` | Assignments | `/api/assignments` | -- | +| `/student/grades` | Grades | `/api/grades` | AiGradeExplainer | +| `/student/attendance` | Attendance | `/api/attendance` | -- | +| `/student/timetable` | Timetable | `/api/timetable` | -- | +| `/student/profile` | Profile | `/api/user` | -- | +| `/student/subject-registration` | **NEW:** Subject Registration | `/api/subject-registrations/available`, `/api/subject-registrations` | -- | +| `/student/courses/:id/chapters/:chapterId` | **NEW:** Chapter View | `/api/chapters/{id}`, `/api/chapters/{chapterId}/materials` | -- | +| `/student/discussions` | **NEW:** Discussion Board | `/api/discussion-boards`, `/api/discussion-boards/{id}/posts` | -- | +| `/student/announcements` | **NEW:** Announcements | `/api/announcements` | -- | +| `/student/messages` | **NEW:** Messages | `/api/messages`, `/api/messages/unread-count` | -- | +| `/student/journey` | **NEW:** Student Journey | `/api/analytics/student`, `/api/proficiency` | -- | + +### 54.3 Teacher Pages (15+) + +| Route | Page | Key APIs | AI Components | +|-------|------|----------|---------------| +| `/teacher/dashboard` | Teacher Dashboard | `/api/user`, `/api/analytics/class` | AiInsightsPanel, AiAlertBanner | +| `/teacher/courses` | Courses | `/api/courses` | -- | +| `/teacher/courses/new` | Course Builder | `/api/courses`, `/api/subjects/{id}/taxonomy` | AiCreationAssistant | +| `/teacher/courses/:id/edit` | Edit Course | `/api/courses/{id}` | AiCreationAssistant | +| `/teacher/assignments` | Assignments | `/api/assignments` | AiCreationAssistant | +| `/teacher/assignments/:id` | Assignment Detail / Grading | `/api/assignments/{id}`, `/api/evaluate/*` | AiGradingAssistant | +| `/teacher/attendance` | Attendance | `/api/attendance` | AiRiskBadge | +| `/teacher/students` | Students | `/api/users/list?type=student` | AiRiskBadge | +| `/teacher/timetable` | Timetable | `/api/timetable` | -- | +| `/teacher/profile` | Profile | `/api/user` | -- | +| `/teacher/courses/:id/chapters` | **NEW:** Course Chapters | `/api/courses/{courseId}/chapters` | -- | +| `/teacher/courses/:id/chapters/:chapterId` | **NEW:** Chapter Detail | `/api/chapters/{id}`, `/api/chapters/{chapterId}/materials` | -- | +| `/teacher/courses/:id/workbench` | **NEW:** AI Workbench | `/api/workbench/*` | AiCreationAssistant | +| `/teacher/courses/:id/discussions` | **NEW:** Discussion Board | `/api/discussion-boards`, `/api/discussion-boards/{id}/posts` | -- | +| `/teacher/announcements` | **NEW:** Announcements | `/api/announcements` | -- | + +### 54.4 Public Pages (4+) + +| Route | Page | Key APIs | AI Components | +|-------|------|----------|---------------| +| `/apply` | **NEW:** Admission Application | `/api/admissions`, `/api/admission-registers` | -- | +| `/apply/status` | **NEW:** Application Status | `/api/admissions/{id}` | -- | +| `/exam/access/{token}` | **NEW:** Official Exam Access (Link) | `/api/exam/access/{token}` | -- | +| `/exam/official/{code}` | **NEW:** Official Exam Access (Landing) | `/api/exam/official-login` | -- | +| `/exam/login/{examId}` | **NEW:** Official Exam Login | `/api/exam/official-login` | -- | +| `/faq` | **NEW:** FAQ | `/api/faq/categories`, `/api/faq/items` | -- | + +### 54.5 Admin Pages (53 -- Verified) + +| Route | Page | Key APIs | AI Components | +|-------|------|----------|---------------| +| `/admin/dashboard` | LMS Dashboard | `/api/analytics/subject`, `/api/courses` | AiInsightsPanel, AiAlertBanner | +| `/admin/platform` | Platform Dashboard | `/api/users/list`, entity counts | AiInsightsPanel | +| `/admin/courses` | Course Management | `/api/courses` | AiCreationAssistant | +| `/admin/students` | Student Management | `/api/users/list?type=student` | AiRiskBadge | +| `/admin/teachers` | Teacher Management | `/api/users/list?type=teacher` | -- | +| `/admin/batches` | Batch Management | `/api/batches` | AiBatchOptimizer | +| `/admin/batches/:id` | Batch Detail | `/api/batches/{id}` | AiBatchOptimizer | +| `/admin/timetable` | Timetable Admin | `/api/timetable` | -- | +| `/admin/reports` | Reports / Analytics | `/api/analytics/*` | AiReportNarrative | +| `/admin/settings` | LMS Settings | Configuration APIs | -- | +| `/admin/assignments` | Assignments | `/api/assignments` | AiCreationAssistant | +| `/admin/examsList` | Exams List + Rubrics | `/api/exam` | -- | +| `/admin/exam-structures` | Exam Structures | `/api/exam-structures` | -- | +| `/admin/rubrics` | Rubrics | `/api/rubrics` | AiCreationAssistant | +| `/admin/generation` | Exam Generation | `/api/exam/*/generate` | AiGeneratorModal | +| `/admin/approval-workflows` | Approval Workflows | `/api/approval-workflows` | AiGradingAssistant | +| `/admin/users` | User Management | `/api/users/list` | -- | +| `/admin/entities` | Entity Management | `/api/entities` | -- | +| `/admin/classrooms` | Classroom Management | `/api/groups` | -- | +| `/admin/taxonomy` | Taxonomy Manager | `/api/subjects`, `/api/domains`, `/api/topics` | -- | +| `/admin/resources` | Resource Manager | `/api/resources`, `/api/analytics/content-gaps` | -- | +| `/admin/adaptive-analytics` | Adaptive Analytics | `/api/analytics/subject`, `/api/analytics/content-gaps` | AiInsightsPanel | +| `/admin/student-performance` | Student Performance | `/api/proficiency/class` | AiStudyCoach, AiGradeExplainer | +| `/admin/stats-corporate` | Corporate Stats | `/api/statistical` | -- | +| `/admin/record` | Record | `/api/stats` | -- | +| `/admin/training/vocabulary` | Vocabulary | `/api/training`, FAISS tips | -- | +| `/admin/training/grammar` | Grammar | `/api/training`, FAISS tips | -- | +| `/admin/payment-record` | Payment Record | `/api/packages`, payment APIs | -- | +| `/admin/tickets` | Tickets | `/api/tickets` | -- | +| `/admin/settings-platform` | Platform Settings | Settings APIs | -- | +| `/admin/exam` | Exam Runner | `/api/exam`, `/api/sessions` | -- | +| `/admin/profile` | Admin Profile | `/api/user` | -- | +| `/admin/academic-years` | Academic Year Manager | `/api/academic-years`, `/api/academic-terms` | -- | +| `/admin/departments` | Department Manager | `/api/departments` | -- | +| `/admin/admissions` | Admission List | `/api/admissions` | -- | +| `/admin/admissions/:id` | Admission Detail | `/api/admissions/{id}` | -- | +| `/admin/admission-register` | Admission Register | `/api/admission-registers` | -- | +| `/admin/exam-sessions` | Institutional Exam Sessions | `/api/inst-exam-sessions`, `/api/inst-exams` | -- | +| `/admin/marksheets` | Marksheet Manager | `/api/marksheets`, `/api/result-templates`, `/api/grade-config` | -- | +| `/admin/faq` | FAQ Manager | `/api/faq/categories`, `/api/faq/items` | -- | +| `/admin/notification-rules` | Notification Rules | `/api/notification-rules` | -- | +| `/admin/approval-config` | Approval Workflow Config | `/api/approval-workflows` | -- | +| `/admin/student-leave` | **BEYOND SRS:** Student Leave Management | `/api/student-leaves`, `/api/student-leave-types` | -- | +| `/admin/fees` | **BEYOND SRS:** Fees Management | `/api/fees-plans`, `/api/student-fees`, `/api/fees-terms` | -- | +| `/admin/lessons` | **BEYOND SRS:** Lessons Management | `/api/lessons` | -- | +| `/admin/gradebook` | **BEYOND SRS:** Gradebook | `/api/gradebooks`, `/api/gradebook-lines`, `/api/grading-assignments` | -- | +| `/admin/student-progress` | **BEYOND SRS:** Student Progress | `/api/student-progress` | -- | +| `/admin/library` | **BEYOND SRS:** Library Management | `/api/library/media`, `/api/library/movements`, `/api/library/cards` | -- | +| `/admin/activities` | **BEYOND SRS:** Activity Management | `/api/activities`, `/api/activity-types` | -- | +| `/admin/facilities` | **BEYOND SRS:** Facility Management | `/api/facilities`, `/api/assets` | -- | +| `/admin/roles-permissions` | **BEYOND SRS:** Roles & Permissions CRUD | `/api/roles`, `/api/permissions` | -- | +| `/admin/user-roles` | **BEYOND SRS:** User Role Assignment | `/api/users/with-roles` | -- | +| `/admin/authority-matrix` | **BEYOND SRS:** Authority Matrix | `/api/authority-matrix` | -- | + +--- + +## 55. Non-Functional Requirements + +| ID | Requirement | +|----|-------------| +| NFR-SEC-01 | Authorize on server/API; do not rely on hiding UI alone | +| NFR-SEC-02 | Students can only access their own proficiency and learning plan data | +| NFR-SEC-03 | AI prompts never include student personal data | +| NFR-SEC-04 | Resource uploads validated for file type (no executables) | +| NFR-PERF-01 | Pagination and server-side filtering for all list views | +| NFR-PERF-02 | Diagnostic question generation < 3 seconds | +| NFR-PERF-03 | AI grading response < 10 seconds (writing/speaking < 60 seconds) | +| NFR-PERF-04 | Learning plan generation < 10 seconds | +| NFR-PERF-05 | AI content generation < 15 seconds | +| NFR-PERF-06 | API response time < 2 seconds for standard CRUD | +| NFR-SCALE-01 | 200 concurrent students per subject | +| NFR-SCALE-02 | 500 topics per subject | +| NFR-SCALE-03 | 1,000 resources per subject | +| NFR-UX-01 | Consistent search/filter/pagination patterns; empty states; loading feedback | +| NFR-UX-02 | Math content rendered with KaTeX; IT code rendered with syntax highlighting | +| NFR-A11Y-01 | Icon-only actions have accessible names | +| NFR-A11Y-02 | All forms have proper labels and validation messages | +| NFR-DATA-01 | Proficiency scores use weighted averages (never direct overwrite) | +| NFR-DATA-02 | Learning plan changes are logged with user ID and timestamp | +| NFR-DATA-03 | Resource deletions are soft-delete to preserve completion records | + +--- + +## Appendix A: Odoo Module Inventory (Verified Against Backend v2) + +The following module inventory reflects the actual deployed modules in `encoach_backend_new_v2`: + +**Custom EnCoach Modules (27):** + +| # | Module | Status | Description | +|---|--------|--------|-------------| +| 1 | `encoach_core` | Deployed | Users, entities, roles, permissions, codes, invites | +| 2 | `encoach_ai` | Deployed | OpenAI service, constants, blacklist, token counting | +| 3 | `encoach_ai_media` | Deployed | TTS (Polly), STT (Whisper), ELAI avatars | +| 4 | `encoach_ai_generation` | Deployed | Exam content generation (multi-subject) | +| 5 | `encoach_ai_grading` | Deployed | Writing/speaking grading (multi-subject rubrics) | +| 6 | `encoach_exam` | Deployed | Exam model with `subject_id`, `topic_ids` | +| 7 | `encoach_assignment` | Deployed | Exam assignments | +| 8 | `encoach_classroom` | Deployed | Classroom groups | +| 9 | `encoach_stats` | Deployed | Sessions and statistics with `topic_id`, `subject_id` | +| 10 | `encoach_training` | Deployed | Training tips and walkthroughs | +| 11 | `encoach_subscription` | Deployed | Packages, payments, discounts | +| 12 | `encoach_registration` | Deployed | User registration and batch import | +| 13 | `encoach_ticket` | Deployed | Support tickets | +| 14 | `encoach_api` | Deployed | REST API controllers (~120 routes: auth, users, entities, roles, exams, assignments, classrooms, storage, stats, subscriptions, tickets, approvals, training, generation, AI analytics) | +| 15 | `encoach_taxonomy` | Deployed | Subject, domain, topic, learning objective models | +| 16 | `encoach_resources` | Deployed | Learning resource library (~7 routes) | +| 17 | `encoach_adaptive` | Deployed | Proficiency, learning plans, progress tracking, content cache | +| 18 | `encoach_adaptive_api` | Deployed | REST controllers for adaptive learning (~41 routes: taxonomy, diagnostic, proficiency, learning plans, content, coaching) | +| 19 | `encoach_adaptive_ai` | Deployed | AI services for diagnostics, plans, coaching, content | +| 20 | `encoach_lms_api` | Deployed | REST API bridge for OpenEduCat + enterprise models (~209 routes) | +| 21 | `encoach_sis` | Deployed | UTAS SIS integration (sync, mapping) | +| 22 | `encoach_branding` | Deployed | Whitelabeling configuration | +| 23 | `encoach_courseware` | Deployed | Course chapters, chapter materials, chapter progress, AI workbench | +| 24 | `encoach_communication` | Deployed | Discussion boards, posts, announcements, messaging | +| 25 | `encoach_notification` | Deployed | Notification engine, notification rules, reminders, preferences | +| 26 | `encoach_faq` | Deployed | FAQ categories and items, role-filtered | +| 27 | `encoach_approval` | Deployed | Approval workflows, stages, requests | + +**OpenEduCat Community Modules (14):** + +| # | Module | Status | Description | +|---|--------|--------|-------------| +| 1 | `openeducat_core` | Deployed | `op.course`, `op.batch`, `op.student`, `op.faculty`, `op.subject`, `op.department`, `op.category`, `op.academic.year`, `op.academic.term`, `op.student.course`, `op.subject.registration` | +| 2 | `openeducat_timetable` | Deployed | `op.session`, `op.timing` | +| 3 | `openeducat_attendance` | Deployed | `op.attendance.register`, `op.attendance.sheet`, `op.attendance.line`, `op.attendance.type` | +| 4 | `openeducat_classroom` | Deployed | `op.classroom` (physical rooms) | +| 5 | `openeducat_exam` | Deployed | `op.exam.session`, `op.exam`, `op.exam.type`, `op.exam.attendees`, `op.exam.room`, `op.marksheet.register`, `op.marksheet.line`, `op.result.line`, `op.result.template`, `op.grade.configuration` | +| 6 | `openeducat_assignment` | Deployed | `op.assignment`, `op.assignment.sub.line`, `grading.assignment`, `grading.assignment.type` | +| 7 | `openeducat_admission` | Deployed | `op.admission`, `op.admission.register` | +| 8 | `openeducat_facility` | Deployed | `op.facility`, `op.facility.line` | +| 9 | `openeducat_fees` | Deployed | `op.fees.plan`, `op.student.fees.details`, `op.fees.terms` | +| 10 | `openeducat_library` | Deployed | `op.media`, `op.media.movement`, `op.library.card` | +| 11 | `openeducat_activity` | Deployed | `op.activity`, `op.activity.type` | +| 12 | `openeducat_parent` | Deployed | Parent-student relationship management | +| 13 | `openeducat_erp` | Deployed | OpenEduCat ERP connector (meta-module) | +| 14 | `theme_web_openeducat` | Deployed | OpenEduCat web theme | + +**Total: 27 custom EnCoach + 14 OpenEduCat = 41 modules** + +--- + +## Appendix B: Implementation Status + +All items have been implemented and deployed to staging. The table below records the original priority assignments. + +| Priority | Components | Status | +|----------|-----------|--------| +| **P0** | Auth, core user management, taxonomy models, resource model | **DONE** | +| **P1** | Diagnostic engine, proficiency profile, learning plan generation | **DONE** | +| **P1** | Exam engine with multi-subject support | **DONE** | +| **P2** | OpenEduCat porting (v17→v19), LMS API bridge, course/batch/timetable/attendance | **DONE** | +| **P2** | Academic years/terms, departments, admission workflow | **DONE** | +| **P2** | Content delivery (hybrid), practice questions, mastery quizzes | **DONE** | +| **P2** | AI components wiring (all 15 frontend components to real backend) | **DONE** | +| **P3** | AI coaching (hints, explanations, study suggestions) | **DONE** | +| **P3** | Spaced repetition, analytics dashboards | **DONE** | +| **P3** | Institutional exam sessions, marksheets, subject registration | **DONE** | +| **P3** | Course assignment submissions (file upload, grading) | **DONE** | +| **P2** | Courseware module (chapters, materials, progress tracking) | **DONE** | +| **P2** | Communication module (discussions, announcements, messaging) | **DONE** | +| **P3** | Notification engine (rules, email integration, reminders) | **DONE** | +| **P3** | Enhanced approval workflows (timed stages, escalation, bypass) | **DONE** | +| **P3** | Plagiarism detection (GPTZero integration) | **DONE** | +| **P3** | Official exam access modes (link, landing page, separate login) | **DONE** | +| **P4** | FAQ system (admin-managed, role-filtered) | **DONE** | +| **P4** | AI Workbench for course content generation | **DONE** | +| **P3** | SIS integration, whitelabeling | **DONE** | +| **P4** | Content gap detection, FAISS tips per subject, batch optimizer | **DONE** | +| **P4** | Payment system, subscription management | **DONE** | +| **--** | **BEYOND SRS:** Student leave management | **DONE** | +| **--** | **BEYOND SRS:** Fees management | **DONE** | +| **--** | **BEYOND SRS:** Lessons management | **DONE** | +| **--** | **BEYOND SRS:** Gradebook and grading assignments | **DONE** | +| **--** | **BEYOND SRS:** Student progress tracking | **DONE** | +| **--** | **BEYOND SRS:** Library management | **DONE** | +| **--** | **BEYOND SRS:** Activity management | **DONE** | +| **--** | **BEYOND SRS:** Facility and asset management | **DONE** | +| **--** | **BEYOND SRS:** Roles CRUD, user role assignment, authority matrix | **DONE** | + +## Appendix C: API Route Summary (Verified) + +| Controller Package | Route Count | Scope | +|-------------------|-------------|-------| +| `encoach_api` | ~120 | Auth, users, entities, roles, exams, assignments, classrooms, storage, stats, subscriptions, tickets, approvals, training, generation, AI analytics | +| `encoach_lms_api` | ~209 | Courses, students, teachers, batches, timetable, attendance, grades, departments, academic, admissions, subject registration, institutional exams, course assignments, student leave, fees, lessons, gradebook, student progress, library, activities, facilities, notifications, FAQ, courseware, plagiarism, communication, classrooms | +| `encoach_adaptive_api` | ~41 | Taxonomy (subjects, domains, topics), diagnostic, proficiency, learning plans, content, coaching | +| `encoach_resources` | ~7 | Learning resources (CRUD, download, complete, rate) | +| **Total** | **~377** | | + +## Appendix D: Frontend File Summary (Verified) + +| Category | Count | Location | +|----------|-------|----------| +| **Root pages** | 28 | `src/pages/*.tsx` | +| **Admin pages** | 33 | `src/pages/admin/*.tsx` | +| **Student pages** | 19 | `src/pages/student/*.tsx` | +| **Teacher pages** | 14 | `src/pages/teacher/*.tsx` | +| **Total pages** | **93** | | +| **Service modules** | 37 | `src/services/*.service.ts` | +| **Type definition files** | 29 | `src/types/*.ts` | +| **Query hook modules** | 21 | `src/hooks/queries/use*.ts` | +| **AI components** | 14 | `src/components/ai/*.tsx` | + +--- + +*This is the definitive SRS for the new EnCoach platform. All previous SRS documents (`SRS-EnCoach.md`, `MATH_IT_ADAPTIVE_LEARNING_SRS.md`, `ODOO_MIGRATION_SRS_v2.md`, `ENCOACH_UNIFIED_SRS.md` v1.0) are superseded by this document for new development.* diff --git a/docs/MATH_IT_ADAPTIVE_LEARNING_SRS.md b/docs/MATH_IT_ADAPTIVE_LEARNING_SRS.md new file mode 100644 index 00000000..343a0f46 --- /dev/null +++ b/docs/MATH_IT_ADAPTIVE_LEARNING_SRS.md @@ -0,0 +1,1412 @@ +# Math & IT Adaptive Learning System -- Software Requirements Specification + +> **SUPERSEDED** -- This document has been merged into `ENCOACH_UNIFIED_SRS.md` (v2.0). The adaptive learning engine for Math and IT is now part of the unified platform specification (Part II: Universal Subject Engine, sections 6-11). All features have been implemented and deployed. + +**Document Version:** 1.0 +**Date:** March 16, 2026 +**Status:** ~~Draft -- Architect Review~~ **SUPERSEDED** +**Author:** EnCoach Engineering Team +**Audience:** Architect (initial review), then development team + +--- + +## Table of Contents + +1. [Introduction and Scope](#1-introduction-and-scope) +2. [System Overview](#2-system-overview) +3. [Subject Taxonomy and Knowledge Graph](#3-subject-taxonomy-and-knowledge-graph) +4. [Diagnostic Assessment Engine](#4-diagnostic-assessment-engine) +5. [Proficiency Profile](#5-proficiency-profile) +6. [Learning Plan Generation](#6-learning-plan-generation) +7. [Hybrid Content Delivery](#7-hybrid-content-delivery) +8. [Progress Tracking and Reassessment](#8-progress-tracking-and-reassessment) +9. [Data Models -- Odoo 19](#9-data-models----odoo-19) +10. [REST API Specification](#10-rest-api-specification) +11. [AI Integration Specification](#11-ai-integration-specification) +12. [Frontend Requirements](#12-frontend-requirements) +13. [Non-Functional Requirements](#13-non-functional-requirements) + +--- + +## 1. Introduction and Scope + +### 1.1 Purpose + +This document specifies the requirements for adding **Mathematics** and **Information Technology** adaptive learning modules to the EnCoach platform. The system implements a competency-based adaptive learning loop: diagnostic assessment determines a student's current level, an AI-generated personalized learning plan guides them through content, and continuous reassessment adjusts the path based on progress. + +### 1.2 Scope + +**In scope:** +- Subject taxonomy and knowledge graph for Math and IT +- Adaptive diagnostic assessment engine (level detection per topic) +- Student proficiency profiling (per-topic mastery tracking) +- AI-powered personalized learning plan generation +- Hybrid content delivery (human-uploaded resources + AI-generated content) +- Progress tracking with mastery-based advancement +- AI coaching (hints, explanations, study suggestions) +- Math-specific features (formula rendering, numerical grading) +- IT-specific features (code highlighting, scenario-based questions) + +**Out of scope:** +- English/IELTS module changes (stays as-is) +- Mobile native applications +- Live tutoring / video conferencing +- Payment or subscription changes +- OpenEduCat LMS integration (separate workstream) + +### 1.3 Relationship to Existing System + +The adaptive learning system is built **on top of** the existing EnCoach Odoo 19 backend. It reuses: + +| Existing Component | Reuse | +|-------------------|-------| +| `encoach.exam` model | Extended with `subject_id` and `topic_ids` for subject-scoped exams. `is_diagnostic` flag already exists. | +| `encoach.stat` model | Score tracking per exercise. Extended with `topic_id` for per-topic analytics. | +| `encoach.session` model | Exam session state tracking, reused as-is. | +| `encoach_ai_generation` service | AI generation patterns reused; new prompt templates for Math/IT. | +| `encoach_ai_grading` service | Grading patterns reused; new rubrics for numerical and keyword grading. | +| `EncoachOpenAIService` | Direct reuse for GPT-4o calls with subject-specific prompts. | +| `EncoachMixin` (API controllers) | Authentication, response formatting, error handling reused as-is. | +| Training tips + FAISS | Architecture reused; new subject-specific knowledge bases. | + +**New Odoo modules required:** + +| Module | Purpose | +|--------|---------| +| `encoach_taxonomy` | Subject, Domain, Topic, Learning Objective models | +| `encoach_adaptive` | Proficiency profile, learning plan, diagnostic engine, progress tracking | +| `encoach_resources` | Human-uploaded learning resources tagged to topics | +| `encoach_adaptive_api` | REST API controllers for all adaptive learning endpoints | +| `encoach_adaptive_ai` | AI services for diagnostic generation, content generation, plan generation, coaching | + +### 1.4 Definitions + +| Term | Definition | +|------|-----------| +| **Subject** | Top-level academic discipline (Mathematics, Information Technology) | +| **Domain** | Major area within a subject (e.g., Algebra, Networking) | +| **Topic** | Specific teachable unit within a domain (e.g., Linear Equations, TCP/IP Model) | +| **Learning Objective** | Measurable skill/knowledge a student should gain from a topic (e.g., "Solve two-variable linear systems") | +| **Mastery Level** | Numeric score (0-100) indicating proficiency on a topic | +| **Mastery Threshold** | Minimum mastery level required to advance (default: 80%) | +| **Diagnostic Assessment** | Adaptive test that determines initial proficiency across all topics in a subject | +| **Learning Plan** | AI-generated, sequenced list of topics for a student to study, respecting prerequisites | +| **Resource** | Human-uploaded learning material (PDF, video, link, document) tagged to one or more topics | +| **Prerequisite** | A topic that must be mastered before another topic becomes available | + +--- + +## 2. System Overview + +### 2.1 The Adaptive Learning Loop + +```mermaid +graph TB + subgraph loop ["Adaptive Learning Loop"] + A["1. Diagnostic Assessment
Adaptive test determines
per-topic proficiency"] --> B["2. Proficiency Profile
Map of student knowledge
gaps and strengths"] + B --> C["3. Learning Plan
AI generates personalized
topic sequence"] + C --> D["4. Content Delivery
Human resources + AI content
+ AI coaching"] + D --> E["5. Progress Tracking
Mastery quizzes, completion
tracking, reassessment"] + E -->|"Profile updated"| B + end + + Entry["Student enrolls
in Subject"] --> A + E -->|"All topics mastered"| Complete["Subject Complete
Certificate issued"] +``` + +### 2.2 Actors + +| Actor | Role in Adaptive Learning | +|-------|--------------------------| +| **Student** | Takes diagnostic assessment, follows learning plan, consumes content, completes mastery quizzes, receives AI coaching | +| **Teacher / Academic Staff** | Defines subject taxonomy (topics, prerequisites), uploads learning resources (PDFs, videos, links), reviews AI-generated content quality, overrides learning plans | +| **Admin** | Manages subjects and domains, configures diagnostic assessment parameters, monitors system analytics, manages resource library | +| **AI Engine** | Generates diagnostic questions, creates learning plans, produces supplementary content, grades assessments, provides contextual coaching | + +### 2.3 Architecture -- New Modules in Context + +```mermaid +graph TB + subgraph frontend ["React Frontend"] + DiagUI["Diagnostic Test UI"] + PlanUI["Learning Plan Dashboard"] + ContentUI["Content Viewer
PDF/Video/AI Content"] + ProgressUI["Progress Tracker"] + CoachUI["AI Coach Panel"] + AdminTaxUI["Taxonomy Admin"] + ResourceUI["Resource Manager"] + end + + subgraph newModules ["New Odoo Modules"] + TaxMod["encoach_taxonomy
Subject, Domain, Topic,
Learning Objective"] + AdaptMod["encoach_adaptive
Proficiency, Learning Plan,
Diagnostic Config"] + ResMod["encoach_resources
Resource Library"] + AdaptAPI["encoach_adaptive_api
REST Controllers"] + AdaptAI["encoach_adaptive_ai
AI Services"] + end + + subgraph existing ["Existing Odoo Modules"] + ExamMod["encoach_exam
Exam model"] + StatMod["encoach_stats
Session, Stat"] + AICore["encoach_ai
OpenAI Service"] + AIGen["encoach_ai_generation
Content Generation"] + AIGrade["encoach_ai_grading
Grading Service"] + Core["encoach_core
Users, Entities"] + end + + subgraph data ["Data Layer"] + PG["PostgreSQL 16"] + end + + subgraph ai ["AI Services"] + GPT["OpenAI GPT-4o"] + FAISS_DB["FAISS Index
per Subject"] + end + + frontend -->|"REST API"| AdaptAPI + AdaptAPI --> TaxMod + AdaptAPI --> AdaptMod + AdaptAPI --> ResMod + AdaptAPI --> AdaptAI + AdaptAI --> AICore + AdaptAI --> GPT + AdaptAI --> FAISS_DB + AdaptMod --> ExamMod + AdaptMod --> StatMod + TaxMod --> PG + AdaptMod --> PG + ResMod --> PG +``` + +--- + +## 3. Subject Taxonomy and Knowledge Graph + +### 3.1 Taxonomy Hierarchy + +The taxonomy is a four-level hierarchy that structures all learning content: + +``` +Subject (e.g., Mathematics) +└── Domain (e.g., Algebra) + └── Topic (e.g., Linear Equations) + └── Learning Objective (e.g., "Solve 2-variable linear systems using substitution") +``` + +Topics form a **directed acyclic graph (DAG)** via prerequisite relationships. A topic may have zero or more prerequisites, and a student cannot begin a topic until all prerequisites are at Proficient level (mastery >= 60%). + +### 3.2 Example Taxonomy -- Mathematics + +``` +Mathematics +├── Arithmetic +│ ├── Number Operations (prerequisites: none) +│ │ ├── LO: Perform addition, subtraction, multiplication, division on integers +│ │ └── LO: Apply order of operations (PEMDAS/BODMAS) +│ ├── Fractions and Decimals (prerequisites: Number Operations) +│ │ ├── LO: Convert between fractions, decimals, and percentages +│ │ └── LO: Perform arithmetic with fractions +│ └── Ratios and Proportions (prerequisites: Fractions and Decimals) +│ └── LO: Solve proportion problems +│ +├── Algebra +│ ├── Algebraic Expressions (prerequisites: Arithmetic/Number Operations) +│ │ ├── LO: Simplify algebraic expressions +│ │ └── LO: Evaluate expressions for given variable values +│ ├── Linear Equations (prerequisites: Algebraic Expressions) +│ │ ├── LO: Solve single-variable linear equations +│ │ └── LO: Solve two-variable systems using substitution and elimination +│ ├── Quadratic Equations (prerequisites: Linear Equations) +│ │ ├── LO: Solve quadratics by factoring +│ │ └── LO: Apply the quadratic formula +│ └── Polynomials (prerequisites: Quadratic Equations) +│ └── LO: Factor and divide polynomials +│ +├── Geometry +│ ├── Basic Shapes (prerequisites: Number Operations) +│ │ └── LO: Calculate area and perimeter of common shapes +│ ├── Angles and Triangles (prerequisites: Basic Shapes) +│ │ └── LO: Apply angle sum property, classify triangles +│ ├── Coordinate Geometry (prerequisites: Linear Equations, Basic Shapes) +│ │ └── LO: Plot points, calculate distance and midpoint +│ └── Trigonometry (prerequisites: Angles and Triangles, Coordinate Geometry) +│ └── LO: Apply sin, cos, tan to right-angled triangles +│ +├── Statistics and Probability +│ ├── Data Representation (prerequisites: Number Operations) +│ │ └── LO: Create and interpret bar charts, histograms, pie charts +│ ├── Measures of Central Tendency (prerequisites: Data Representation) +│ │ └── LO: Calculate mean, median, mode for data sets +│ └── Basic Probability (prerequisites: Fractions and Decimals) +│ └── LO: Calculate probability of simple and compound events +│ +└── Calculus (advanced, optional) + ├── Limits (prerequisites: Polynomials, Coordinate Geometry) + ├── Derivatives (prerequisites: Limits) + └── Integrals (prerequisites: Derivatives) +``` + +### 3.3 Example Taxonomy -- Information Technology + +``` +Information Technology +├── Computer Fundamentals +│ ├── Hardware Components (prerequisites: none) +│ │ └── LO: Identify and describe CPU, RAM, storage, I/O devices +│ ├── Operating Systems (prerequisites: Hardware Components) +│ │ └── LO: Explain OS functions, file systems, process management +│ └── Number Systems (prerequisites: none) +│ └── LO: Convert between binary, decimal, hexadecimal +│ +├── Networking +│ ├── Network Basics (prerequisites: Computer Fundamentals/Hardware) +│ │ └── LO: Describe LAN, WAN, MAN topologies +│ ├── TCP/IP Model (prerequisites: Network Basics) +│ │ └── LO: Explain the 4 layers of TCP/IP with protocols +│ ├── IP Addressing (prerequisites: TCP/IP Model, Number Systems) +│ │ └── LO: Calculate subnets, identify public/private addresses +│ └── Network Security (prerequisites: IP Addressing) +│ └── LO: Describe firewalls, encryption, VPN concepts +│ +├── Databases +│ ├── Database Concepts (prerequisites: Computer Fundamentals/OS) +│ │ └── LO: Explain RDBMS, tables, keys, relationships +│ ├── SQL Fundamentals (prerequisites: Database Concepts) +│ │ └── LO: Write SELECT, INSERT, UPDATE, DELETE queries +│ ├── Database Design (prerequisites: SQL Fundamentals) +│ │ └── LO: Apply normalization (1NF, 2NF, 3NF) +│ └── Advanced SQL (prerequisites: Database Design) +│ └── LO: Write JOINs, subqueries, aggregate functions +│ +├── Programming +│ ├── Programming Logic (prerequisites: none) +│ │ └── LO: Design algorithms using flowcharts and pseudocode +│ ├── Python Basics (prerequisites: Programming Logic) +│ │ └── LO: Write programs using variables, loops, conditionals +│ ├── Data Structures (prerequisites: Python Basics) +│ │ └── LO: Implement lists, stacks, queues, dictionaries +│ └── Object-Oriented Programming (prerequisites: Data Structures) +│ └── LO: Apply classes, inheritance, polymorphism +│ +└── Cybersecurity + ├── Security Fundamentals (prerequisites: Network Security) + │ └── LO: Identify common threats, vulnerabilities, attack vectors + └── Cryptography Basics (prerequisites: Security Fundamentals, Number Systems) + └── LO: Explain symmetric/asymmetric encryption, hashing +``` + +### 3.4 Prerequisite Graph Visualization + +```mermaid +graph LR + subgraph mathPrereqs ["Math Prerequisites (simplified)"] + NumOps["Number Operations"] --> FracDec["Fractions & Decimals"] + FracDec --> Ratios["Ratios & Proportions"] + NumOps --> AlgExpr["Algebraic Expressions"] + AlgExpr --> LinEq["Linear Equations"] + LinEq --> QuadEq["Quadratic Equations"] + QuadEq --> Poly["Polynomials"] + NumOps --> BasicShapes["Basic Shapes"] + BasicShapes --> Angles["Angles & Triangles"] + LinEq --> CoordGeom["Coordinate Geometry"] + BasicShapes --> CoordGeom + Angles --> Trig["Trigonometry"] + CoordGeom --> Trig + Poly --> Limits["Limits"] + CoordGeom --> Limits + Limits --> Deriv["Derivatives"] + Deriv --> Integ["Integrals"] + end +``` + +### 3.5 Taxonomy Management + +| Action | Actor | Method | +|--------|-------|--------| +| Create/edit subjects | Admin | Admin UI or API | +| Create/edit domains | Admin / Academic Staff | Admin UI or API | +| Create/edit topics | Academic Staff | Admin UI or API, including prerequisite mapping | +| Create/edit learning objectives | Academic Staff | Admin UI, attached to topics | +| Import taxonomy from CSV/JSON | Admin | Bulk import endpoint | +| AI-suggest sub-topics | Academic Staff | Given a domain, AI suggests topic breakdown; staff reviews and approves | + +--- + +## 4. Diagnostic Assessment Engine + +### 4.1 Purpose + +When a student begins a new subject, a diagnostic assessment determines their current proficiency across all domains and topics. This is the input for learning plan generation. + +### 4.2 Adaptive Assessment Algorithm + +The diagnostic uses a simplified **Computer Adaptive Testing (CAT)** approach: + +```mermaid +sequenceDiagram + participant Student + participant System as Diagnostic Engine + participant AI as GPT-4o + participant DB as Proficiency Store + + Student->>System: Start diagnostic for "Mathematics" + System->>System: Load topic list for Mathematics (all domains) + System->>System: Select first domain, set difficulty = MEDIUM + + loop For each domain (round-robin) + System->>AI: Generate question for topic X at difficulty MEDIUM + AI-->>System: Question + correct answer + rubric + System->>Student: Present question + Student->>System: Submit answer + System->>System: Grade answer (auto or AI) + + alt Correct + System->>System: Increase difficulty for this domain + System->>System: Add points to related topics + else Incorrect + System->>System: Decrease difficulty for this domain + System->>System: Mark gap in related topics + end + end + + System->>DB: Store per-topic mastery scores + System->>Student: Show proficiency profile summary +``` + +### 4.3 Algorithm Parameters + +| Parameter | Default | Configurable | Description | +|-----------|---------|-------------|-------------| +| `questions_per_domain` | 4 | Yes (admin) | Number of questions asked per domain | +| `total_question_cap` | 25 | Yes (admin) | Maximum total questions in diagnostic | +| `time_limit_minutes` | 45 | Yes (admin) | Time limit for full diagnostic | +| `starting_difficulty` | `medium` | Yes (admin) | Initial difficulty level | +| `difficulty_levels` | `[easy, medium, hard, advanced]` | No | Fixed scale | +| `mastery_per_correct` | +25 | Yes (admin) | Points added to topic mastery for correct answer | +| `mastery_per_incorrect` | -10 | Yes (admin) | Points subtracted for incorrect answer | +| `min_mastery` | 0 | No | Floor value | +| `max_mastery` | 100 | No | Ceiling value | + +### 4.4 Question Types per Subject + +**Mathematics:** + +| Question Type | Format | Example | Grading Method | +|--------------|--------|---------|---------------| +| `multiple_choice` | 4 options, 1 correct | "What is the slope of y = 3x + 2?" (a) 2 (b) 3 (c) 5 (d) 1 | Exact match | +| `numerical` | Student enters a number | "Solve for x: 2x + 6 = 14" → 4 | Numerical tolerance (default: 0.01) | +| `short_answer` | Student types answer | "Factor x² - 9" → "(x+3)(x-3)" | AI-graded (GPT-4o evaluates equivalence) | +| `fill_blanks` | Complete the equation | "The area of a circle is π___²" → r | Exact match (case-insensitive) | +| `worked_problem` | Multi-step solution | "Find the roots of 2x² + 5x - 3 = 0. Show your work." | AI-graded (GPT-4o evaluates steps and final answer) | + +**Information Technology:** + +| Question Type | Format | Example | Grading Method | +|--------------|--------|---------|---------------| +| `multiple_choice` | 4 options, 1 correct | "Which layer of TCP/IP handles routing?" (a) Application (b) Transport (c) Internet (d) Link | Exact match | +| `true_false` | True or False | "A primary key can contain NULL values." → False | Exact match | +| `short_answer` | Student types answer | "What SQL command retrieves data from a table?" → SELECT | Keyword match (case-insensitive, AI-assisted) | +| `code_completion` | Complete code snippet | "Write a Python function that returns the sum of a list" | AI-graded (GPT-4o evaluates correctness and approach) | +| `scenario` | Read scenario, answer question | "Given this network diagram, identify the subnet..." | AI-graded | + +### 4.5 Diagnostic Configuration Model + +Each subject has a diagnostic configuration that defines the assessment behavior: + +```python +# Diagnostic configuration per subject +{ + "subject_id": 1, # Mathematics + "questions_per_domain": 4, + "total_question_cap": 25, + "time_limit_minutes": 45, + "starting_difficulty": "medium", + "question_type_weights": { + "multiple_choice": 0.4, # 40% of questions + "numerical": 0.3, # 30% (Math-specific) + "short_answer": 0.2, # 20% + "fill_blanks": 0.1 # 10% + }, + "mastery_scoring": { + "correct_easy": 15, + "correct_medium": 25, + "correct_hard": 35, + "correct_advanced": 45, + "incorrect_penalty": -10 + } +} +``` + +### 4.6 Relationship to Existing Exam System + +The diagnostic assessment reuses the existing `encoach.exam` model: + +- `module` field: new values `math_diagnostic`, `it_diagnostic` added to `MODULE_SELECTION` +- `is_diagnostic` field: already exists, set to `True` +- `parts` field (JSON): stores the adaptive question set +- `encoach.stat`: records per-question scores, linked to `topic_id` +- `encoach.session`: wraps the full diagnostic session + +The key difference from regular exams: diagnostic questions are generated **on-the-fly** during the assessment (adaptive), not pre-generated as a fixed exam. The `parts` JSON is built dynamically as the student progresses. + +--- + +## 5. Proficiency Profile + +### 5.1 Overview + +Every student has a proficiency profile per subject -- a map of their mastery level for every topic in the subject's taxonomy. This profile is the core data structure that drives learning plan generation. + +### 5.2 Mastery Levels + +| Level | Score Range | Description | Visual | +|-------|-----------|-------------|--------| +| **Not Started** | 0-19% | Topic not yet assessed or extremely weak | Grey | +| **Beginner** | 20-39% | Basic awareness, significant gaps | Red | +| **Developing** | 40-59% | Partial understanding, needs practice | Orange | +| **Proficient** | 60-79% | Good understanding, minor gaps | Yellow | +| **Mastered** | 80-100% | Strong command, ready to advance | Green | + +### 5.3 Profile Updates + +The proficiency profile updates in these scenarios: + +| Trigger | Update Logic | +|---------|-------------| +| **Diagnostic assessment** | Initial scores set per topic based on diagnostic performance | +| **Practice exercises** | Mastery adjusts based on exercise results: +5 per correct, -2 per incorrect (smaller deltas than diagnostic) | +| **Mastery quiz** | Major update: if quiz score >= mastery threshold, topic moves to Mastered. If below, mastery recalculated as weighted average of current mastery and quiz score. | +| **Time decay** | If a student hasn't engaged with a topic for N days (configurable, default: 30), mastery decays by 5% per week of inactivity, down to a floor of the last quiz score minus 20%. Triggers review recommendation. | + +### 5.4 Profile Data Structure + +```python +# Per-student proficiency profile (stored per subject) +{ + "student_id": 42, + "subject_id": 1, # Mathematics + "overall_mastery": 54.2, # Weighted average across all topics + "last_diagnostic": "2026-04-15T10:30:00Z", + "topics": [ + { + "topic_id": 101, + "topic_name": "Number Operations", + "domain": "Arithmetic", + "mastery": 92, + "level": "mastered", + "last_assessed": "2026-04-15T10:30:00Z", + "assessment_count": 3, + "time_spent_minutes": 45, + "decay_due": "2026-05-15T10:30:00Z" + }, + { + "topic_id": 105, + "topic_name": "Linear Equations", + "domain": "Algebra", + "mastery": 35, + "level": "beginner", + "last_assessed": "2026-04-15T10:35:00Z", + "assessment_count": 1, + "time_spent_minutes": 0, + "prerequisites_met": true + }, + { + "topic_id": 106, + "topic_name": "Quadratic Equations", + "domain": "Algebra", + "mastery": 0, + "level": "not_started", + "last_assessed": null, + "assessment_count": 0, + "time_spent_minutes": 0, + "prerequisites_met": false, + "blocked_by": ["Linear Equations"] + } + ] +} +``` + +--- + +## 6. Learning Plan Generation + +### 6.1 Overview + +After the diagnostic assessment produces a proficiency profile, the AI generates a personalized learning plan. The plan is an ordered sequence of topics the student should study, respecting prerequisites and prioritizing the weakest areas. + +### 6.2 Plan Generation Algorithm + +```mermaid +graph TB + Input["Inputs:
1. Proficiency Profile
2. Prerequisite Graph
3. Time Constraints"] --> Sort["Topological Sort
Respect prerequisites"] + Sort --> Filter["Filter Out
Already Mastered topics
(mastery >= 80%)"] + Filter --> Prioritize["Prioritize by:
1. Prerequisite readiness
2. Lowest mastery first
3. Domain balance"] + Prioritize --> Estimate["Estimate Duration
per topic based on:
- Current mastery
- Topic complexity
- Historical data"] + Estimate --> AI["GPT-4o Refinement
Natural language plan
with study advice"] + AI --> Plan["Learning Plan
Ordered topic list
with milestones"] +``` + +### 6.3 Plan Generation Logic + +**Step 1: Topological Sort** +- Order all topics respecting prerequisite dependencies +- Topics with no unmet prerequisites come first + +**Step 2: Filter Mastered Topics** +- Exclude topics where mastery >= 80% (already mastered) +- Exclude topics where all learning objectives are met + +**Step 3: Prioritize** +- Among available topics (prerequisites met, not mastered): + 1. Lower mastery = higher priority + 2. Balance across domains (don't cluster all Algebra before any Geometry) + 3. Prerequisite chains: if mastering topic A unlocks 3 more topics, prioritize A + +**Step 4: Estimate Duration** +- Base estimate from topic's `estimated_hours` (set by academic staff) +- Adjusted by current mastery: if mastery is 60%, student needs ~40% of full topic time +- If mastery is 0%, student needs 100% of estimated time +- Historical data: if similar students averaged 2.5 hours, use that + +**Step 5: AI Refinement** +- GPT-4o receives the ordered topic list with mastery scores +- Generates a natural-language study plan with: + - Motivational framing ("You're already strong in Arithmetic, let's build on that...") + - Specific advice per topic ("Focus on the substitution method for Linear Equations") + - Milestone suggestions ("Complete Algebra foundations by Week 2") + +### 6.4 Plan Structure + +```python +# Learning Plan +{ + "plan_id": 1, + "student_id": 42, + "subject_id": 1, + "status": "active", # active, completed, paused + "created_at": "2026-04-15T11:00:00Z", + "target_completion": "2026-06-15T00:00:00Z", + "ai_summary": "Based on your diagnostic, you have a solid foundation in Arithmetic...", + "overall_progress": 23.5, # percentage + "items": [ + { + "sequence": 1, + "topic_id": 103, + "topic_name": "Fractions and Decimals", + "domain": "Arithmetic", + "status": "completed", # locked, available, in_progress, completed + "current_mastery": 85, + "target_mastery": 80, + "estimated_hours": 1.5, + "actual_hours": 1.2, + "started_at": "2026-04-16T09:00:00Z", + "completed_at": "2026-04-16T10:12:00Z" + }, + { + "sequence": 2, + "topic_id": 105, + "topic_name": "Linear Equations", + "domain": "Algebra", + "status": "in_progress", + "current_mastery": 45, + "target_mastery": 80, + "estimated_hours": 3.0, + "actual_hours": 1.1, + "started_at": "2026-04-17T14:00:00Z", + "completed_at": null + }, + { + "sequence": 3, + "topic_id": 106, + "topic_name": "Quadratic Equations", + "domain": "Algebra", + "status": "locked", + "current_mastery": 0, + "target_mastery": 80, + "estimated_hours": 4.0, + "blocked_by": ["Linear Equations"] + } + ] +} +``` + +### 6.5 Plan Adjustments + +| Trigger | Adjustment | +|---------|-----------| +| Student masters a topic faster than estimated | Next topic unlocks early; remaining estimates tightened | +| Student struggles with a topic (3+ failed mastery quizzes) | Additional resources surfaced; estimated time increased; AI coaching intensified | +| Student skips ahead (teacher override) | Prerequisites bypassed; warning logged | +| New topics added to taxonomy | Plan regenerated to include new topics if prerequisites are met | +| Student hasn't engaged for 7+ days | Notification sent; if 14+ days, plan paused and review recommended | + +### 6.6 Teacher Override + +Teachers can: +- Reorder topics in a student's plan +- Add or remove specific topics +- Override prerequisite locks ("allow student to skip to Quadratic Equations") +- Set custom target dates +- Force a plan regeneration + +All overrides are logged with the teacher's user ID and timestamp. + +--- + +## 7. Hybrid Content Delivery + +### 7.1 Content Resolution Order + +When a student reaches a topic in their learning plan, the system serves content in this priority order: + +```mermaid +graph TB + TopicStart["Student opens Topic"] --> CheckRes["Check: Human-uploaded
resources exist?"] + CheckRes -->|"Yes"| ServeHuman["Serve human resources
(PDFs, videos, links)"] + CheckRes -->|"No"| CheckAI["Check: AI can generate
for this topic?"] + CheckAI -->|"Yes"| GenAI["AI generates:
- Explanation
- Worked examples
- Summary"] + CheckAI -->|"No"| FlagGap["Flag content gap
Notify academic staff"] + ServeHuman --> SupplementAI["AI supplements with:
- Practice questions
- Quick summary
- Key takeaways"] + GenAI --> Practice["AI generates
practice exercises"] + SupplementAI --> Practice + Practice --> MasteryQuiz["Mastery Quiz
(AI-generated, 5-10 questions)"] +``` + +### 7.2 Human-Uploaded Resources + +| Attribute | Detail | +|-----------|--------| +| **Types** | PDF, video (URL or upload), external link, document (DOCX, PPTX), interactive (embedded HTML) | +| **Tagging** | Each resource is tagged to one or more topics and optionally to specific learning objectives | +| **Metadata** | Title, description, author, upload date, estimated reading/viewing time, difficulty level | +| **Ordering** | Academic staff sets display order within a topic (sequence field) | +| **Versioning** | Resources can be updated; previous versions retained | +| **Review** | Optional review workflow before resources are visible to students | + +### 7.3 AI-Generated Content + +When no human resources exist or as supplementary material, the AI generates: + +| Content Type | Generation Method | When Used | +|-------------|------------------|-----------| +| **Topic Explanation** | GPT-4o generates a comprehensive explanation of the topic, tailored to the student's proficiency level | When student first opens a topic | +| **Worked Examples** | GPT-4o generates 2-3 step-by-step worked examples relevant to the topic | After explanation, before practice | +| **Key Takeaways** | GPT-4o summarizes the most important points in 3-5 bullet points | After content consumption | +| **Practice Questions** | GPT-4o generates 5-10 questions at appropriate difficulty | After content, before mastery quiz | +| **Hint / Explanation** | GPT-4o explains why an answer is correct/incorrect | After each practice question | + +AI-generated content is **cached** on the first generation and served from cache on subsequent requests. Academic staff can review, edit, or replace cached AI content. + +### 7.4 AI Coaching + +The AI coach is an always-available assistant that provides contextual support: + +| Coaching Action | Trigger | AI Behavior | +|----------------|---------|-------------| +| **Answer explanation** | Student answers a practice question | Explains why the answer is correct or incorrect, references the relevant concept | +| **Hint** | Student requests a hint during practice | Provides a partial clue without giving the full answer | +| **Study suggestion** | Student completes a topic section | Suggests what to focus on next based on performance | +| **Motivational nudge** | Student has been inactive for 3+ days | Personalized message encouraging return to study | +| **Weakness analysis** | Student fails a mastery quiz | Identifies specific concepts within the topic that need more practice | +| **Formula reference** | Student is working on a Math problem (on demand) | Displays relevant formulas for the current topic | + +### 7.5 Content Gap Detection + +The system automatically identifies topics with insufficient learning materials: + +| Condition | Action | +|-----------|--------| +| Topic has 0 human resources and no AI content cached | Flag as "Content Gap -- Critical" and notify admin | +| Topic has human resources but they are >12 months old | Flag as "Review Needed" and notify academic staff | +| AI-generated content for a topic has been rated poorly by students (avg < 3/5) | Flag for human review/replacement | +| A domain has <50% resource coverage across its topics | Flag as "Domain Gap" on admin dashboard | + +--- + +## 8. Progress Tracking and Reassessment + +### 8.1 What Is Tracked + +| Metric | Granularity | Storage | +|--------|------------|---------| +| **Resource completion** | Per resource per student | Binary (viewed/not viewed) + timestamp + time spent | +| **Practice exercise results** | Per question per student | Score, answer given, correct answer, time taken | +| **Mastery quiz results** | Per topic per student | Score, pass/fail, attempt number | +| **Topic time spent** | Per topic per student | Cumulative minutes | +| **Learning plan progress** | Per plan per student | % of topics completed, % of total estimated hours completed | +| **Login frequency** | Per student | Days active, streak count | + +### 8.2 Mastery Quiz + +After a student consumes content for a topic, they take a **mastery quiz** to prove proficiency: + +| Parameter | Value | +|-----------|-------| +| **Questions** | 5-10 (configurable per subject) | +| **Question types** | Mix of types appropriate for the subject (see Section 4.4) | +| **Time limit** | 15 minutes (configurable) | +| **Pass threshold** | 80% (configurable -- "mastery threshold") | +| **Attempts** | Unlimited, but each attempt generates new questions | +| **Cooldown** | 1 hour between attempts (configurable) | +| **On pass** | Topic status → "completed", mastery updated, next topic unlocked | +| **On fail** | Mastery recalculated, AI coaching suggests what to review, additional resources recommended | + +### 8.3 Spaced Repetition + +To ensure long-term retention, mastered topics resurface for review: + +| Days Since Mastery | Review Action | +|-------------------|---------------| +| 7 days | Light review: 3 quick-recall questions | +| 30 days | Medium review: 5 questions at original difficulty | +| 90 days | Full review: equivalent to mastery quiz | + +If a student fails a spaced repetition review, the topic mastery is reduced and it re-enters the active learning plan. + +### 8.4 Analytics Dashboard + +The following analytics are available to different roles: + +**Student Dashboard:** +- Overall subject mastery (percentage, visual progress bar) +- Per-domain mastery breakdown (radar chart) +- Learning plan progress (completed / in progress / locked) +- Study streak and time invested +- Upcoming reviews (spaced repetition schedule) + +**Teacher Dashboard:** +- Class-level mastery distribution per topic (heatmap) +- Students at risk (low mastery, low engagement) +- Content gap report +- Average time per topic vs. estimate + +**Admin Dashboard:** +- Subject-level statistics (enrollments, completion rates, average mastery) +- AI content generation usage and quality metrics +- Resource utilization (most/least used resources) +- System-wide performance trends + +--- + +## 9. Data Models -- Odoo 19 + +### 9.1 New Module: `encoach_taxonomy` + +#### `encoach.subject` + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `Char`, required | Subject name (e.g., "Mathematics", "Information Technology") | +| `code` | `Char`, required, unique | Short code (e.g., "MATH", "IT") | +| `description` | `Text` | Subject description | +| `is_active` | `Boolean`, default True | Whether subject is available to students | +| `icon` | `Char` | Icon identifier for frontend display | +| `domain_ids` | `One2many` → `encoach.domain` | Domains in this subject | +| `diagnostic_config` | `Json` | Diagnostic assessment configuration (see Section 4.5) | +| `mastery_threshold` | `Integer`, default 80 | Minimum mastery % to mark a topic as completed | +| `grading_scale` | `Selection`: `percentage`, `letter`, `custom` | How grades are displayed | +| `grading_scale_config` | `Json` | Custom grade boundaries (e.g., `{"A": 90, "B": 80, ...}`) | + +#### `encoach.domain` + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `Char`, required | Domain name (e.g., "Algebra", "Networking") | +| `code` | `Char`, required | Short code | +| `subject_id` | `Many2one` → `encoach.subject`, required, ondelete=cascade | Parent subject | +| `sequence` | `Integer`, default 10 | Display order within subject | +| `description` | `Text` | Domain description | +| `topic_ids` | `One2many` → `encoach.topic` | Topics in this domain | + +#### `encoach.topic` + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `Char`, required | Topic name (e.g., "Linear Equations") | +| `code` | `Char`, required | Short code | +| `domain_id` | `Many2one` → `encoach.domain`, required, ondelete=cascade | Parent domain | +| `sequence` | `Integer`, default 10 | Display order within domain | +| `description` | `Text` | Topic description | +| `estimated_hours` | `Float`, default 2.0 | Estimated study time in hours | +| `difficulty_level` | `Selection`: `beginner`, `intermediate`, `advanced` | Topic complexity | +| `prerequisite_ids` | `Many2many` → `encoach.topic` (self-referential, rel table `encoach_topic_prerequisite_rel`) | Topics that must be mastered before this one | +| `objective_ids` | `One2many` → `encoach.learning.objective` | Learning objectives | +| `resource_ids` | `Many2many` → `encoach.resource` | Tagged resources | +| `question_type_weights` | `Json` | Override default question type distribution for this topic | + +#### `encoach.learning.objective` + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `Char`, required | Objective description (e.g., "Solve 2-variable linear systems") | +| `topic_id` | `Many2one` → `encoach.topic`, required, ondelete=cascade | Parent topic | +| `bloom_level` | `Selection`: `remember`, `understand`, `apply`, `analyze`, `evaluate`, `create` | Bloom's taxonomy level | +| `sequence` | `Integer`, default 10 | Order within topic | + +### 9.2 New Module: `encoach_resources` + +#### `encoach.resource` + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `Char`, required | Resource title | +| `description` | `Text` | Resource description | +| `resource_type` | `Selection`: `pdf`, `video`, `link`, `document`, `interactive` | Content type | +| `file` | `Binary`, attachment=True | Uploaded file (for pdf, document) | +| `file_name` | `Char` | Original file name | +| `url` | `Char` | External URL (for video, link) | +| `topic_ids` | `Many2many` → `encoach.topic` (rel table `encoach_resource_topic_rel`) | Tagged topics | +| `objective_ids` | `Many2many` → `encoach.learning.objective` | Tagged objectives (optional, finer granularity) | +| `author_id` | `Many2one` → `res.users` | Who uploaded | +| `estimated_minutes` | `Integer` | Estimated consumption time | +| `difficulty_level` | `Selection`: `beginner`, `intermediate`, `advanced` | Resource difficulty | +| `sequence` | `Integer`, default 10 | Display order within topic | +| `is_published` | `Boolean`, default True | Visibility to students | +| `review_status` | `Selection`: `draft`, `reviewed`, `approved` | Review workflow status | + +### 9.3 New Module: `encoach_adaptive` + +#### `encoach.proficiency` + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | `Many2one` → `res.users`, required, ondelete=cascade, index=True | Student | +| `topic_id` | `Many2one` → `encoach.topic`, required, ondelete=cascade, index=True | Topic | +| `subject_id` | `Many2one` → `encoach.subject`, related='topic_id.domain_id.subject_id', store=True | Denormalized for filtering | +| `mastery` | `Float`, default 0 | Current mastery score (0-100) | +| `mastery_level` | `Selection`: `not_started`, `beginner`, `developing`, `proficient`, `mastered` | Computed from mastery score | +| `last_assessed` | `Datetime` | Last assessment date | +| `assessment_count` | `Integer`, default 0 | Number of assessments taken | +| `time_spent_minutes` | `Integer`, default 0 | Cumulative study time | +| `decay_due` | `Datetime` | When mastery decay is next applied | +| `last_quiz_score` | `Float` | Most recent mastery quiz score | + +**SQL constraint:** unique on `(student_id, topic_id)` + +**Computed field logic for `mastery_level`:** +```python +if mastery >= 80: return 'mastered' +elif mastery >= 60: return 'proficient' +elif mastery >= 40: return 'developing' +elif mastery >= 20: return 'beginner' +else: return 'not_started' +``` + +#### `encoach.learning.plan` + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | `Many2one` → `res.users`, required, ondelete=cascade, index=True | Student | +| `subject_id` | `Many2one` → `encoach.subject`, required, ondelete=cascade | Subject | +| `status` | `Selection`: `active`, `completed`, `paused`, `regenerating` | Plan status | +| `created_at` | `Datetime`, default now | Creation timestamp | +| `target_completion` | `Date` | Target completion date | +| `ai_summary` | `Text` | GPT-generated study plan summary | +| `overall_progress` | `Float`, default 0 | Percentage complete (computed) | +| `item_ids` | `One2many` → `encoach.learning.plan.item` | Ordered plan items | +| `override_by` | `Many2one` → `res.users` | If manually overridden, who did it | +| `override_at` | `Datetime` | Override timestamp | + +#### `encoach.learning.plan.item` + +| Field | Type | Description | +|-------|------|-------------| +| `plan_id` | `Many2one` → `encoach.learning.plan`, required, ondelete=cascade | Parent plan | +| `topic_id` | `Many2one` → `encoach.topic`, required | Topic to study | +| `sequence` | `Integer`, required | Order in the plan | +| `status` | `Selection`: `locked`, `available`, `in_progress`, `completed`, `skipped` | Item status | +| `target_mastery` | `Float`, default 80 | Mastery goal for this item | +| `current_mastery` | `Float`, related to proficiency record | Current mastery (denormalized) | +| `estimated_hours` | `Float` | AI-estimated study hours | +| `actual_hours` | `Float`, default 0 | Actual time spent | +| `started_at` | `Datetime` | When student started | +| `completed_at` | `Datetime` | When student completed | +| `quiz_attempts` | `Integer`, default 0 | Number of mastery quiz attempts | + +#### `encoach.resource.completion` + +| Field | Type | Description | +|-------|------|-------------| +| `student_id` | `Many2one` → `res.users`, required, ondelete=cascade | Student | +| `resource_id` | `Many2one` → `encoach.resource`, required, ondelete=cascade | Resource | +| `viewed` | `Boolean`, default False | Has student viewed/opened the resource | +| `viewed_at` | `Datetime` | When first viewed | +| `time_spent_minutes` | `Integer`, default 0 | Time spent on resource | +| `rating` | `Integer` | Student's rating (1-5 stars) | +| `feedback` | `Text` | Optional student feedback | + +#### `encoach.ai.content.cache` + +| Field | Type | Description | +|-------|------|-------------| +| `topic_id` | `Many2one` → `encoach.topic`, required, ondelete=cascade | Topic | +| `content_type` | `Selection`: `explanation`, `worked_example`, `key_takeaways`, `summary` | Type of AI content | +| `difficulty_level` | `Selection`: `beginner`, `intermediate`, `advanced` | Target difficulty | +| `content` | `Text` | Generated content (Markdown) | +| `content_html` | `Html` | Rendered HTML version | +| `generated_at` | `Datetime`, default now | Generation timestamp | +| `model_used` | `Char` | GPT model used (e.g., "gpt-4o") | +| `quality_rating` | `Float` | Average student rating | +| `review_status` | `Selection`: `auto`, `reviewed`, `approved`, `rejected` | Staff review status | +| `reviewed_by` | `Many2one` → `res.users` | Who reviewed | + +### 9.4 Extensions to Existing Models + +#### `encoach.exam` -- add fields + +| Field | Type | Description | +|-------|------|-------------| +| `subject_id` | `Many2one` → `encoach.subject` | Subject this exam belongs to (null for legacy English exams) | +| `topic_ids` | `Many2many` → `encoach.topic` | Topics covered by this exam | + +#### `encoach.stat` -- add fields + +| Field | Type | Description | +|-------|------|-------------| +| `topic_id` | `Many2one` → `encoach.topic` | Topic this stat relates to | +| `subject_id` | `Many2one` → `encoach.subject` | Subject (denormalized) | + +#### `MODULE_SELECTION` -- add values + +Add to the existing selection: `math`, `it`, `math_diagnostic`, `it_diagnostic`, `math_mastery`, `it_mastery` + +--- + +## 10. REST API Specification + +All endpoints follow existing patterns: `@http.route`, `auth='public'`, JWT via `_authenticate()`, responses via `_json_response()` / `_error_response()`. JSON body uses camelCase keys. + +### 10.1 Taxonomy Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/subjects` | List all active subjects | JWT | +| `GET` | `/api/subjects/` | Get subject with domains and topics | JWT | +| `POST` | `/api/subjects` | Create subject | JWT (admin) | +| `PATCH` | `/api/subjects/` | Update subject | JWT (admin) | +| `DELETE` | `/api/subjects/` | Delete subject | JWT (admin) | +| `GET` | `/api/subjects//taxonomy` | Get full taxonomy tree (domains > topics > objectives) | JWT | +| `POST` | `/api/subjects//taxonomy/import` | Bulk import taxonomy from JSON/CSV | JWT (admin) | + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/domains` | List domains (filterable by `subjectId`) | JWT | +| `POST` | `/api/domains` | Create domain | JWT (admin/staff) | +| `PATCH` | `/api/domains/` | Update domain | JWT (admin/staff) | +| `DELETE` | `/api/domains/` | Delete domain | JWT (admin/staff) | + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/topics` | List topics (filterable by `domainId`, `subjectId`) | JWT | +| `GET` | `/api/topics/` | Get topic with objectives and resources | JWT | +| `POST` | `/api/topics` | Create topic | JWT (admin/staff) | +| `PATCH` | `/api/topics/` | Update topic (including prerequisites) | JWT (admin/staff) | +| `DELETE` | `/api/topics/` | Delete topic | JWT (admin/staff) | + +### 10.2 Resource Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/resources` | List resources (filterable by `topicId`, `type`, `published`) | JWT | +| `GET` | `/api/resources/` | Get resource detail | JWT | +| `POST` | `/api/resources` | Upload resource (multipart for files) | JWT (staff) | +| `PATCH` | `/api/resources/` | Update resource metadata | JWT (staff) | +| `DELETE` | `/api/resources/` | Delete resource | JWT (staff) | +| `GET` | `/api/resources//download` | Download resource file | JWT | +| `POST` | `/api/resources//complete` | Mark resource as viewed/completed by current student | JWT | +| `POST` | `/api/resources//rate` | Rate resource (1-5 stars) | JWT | + +### 10.3 Diagnostic Assessment Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `POST` | `/api/diagnostic/start` | Start diagnostic for a subject. Body: `{ "subjectId": 1 }`. Returns first question batch. | JWT | +| `POST` | `/api/diagnostic/answer` | Submit answer, get next question. Body: `{ "diagnosticSessionId": "...", "questionId": "...", "answer": "..." }`. Returns: grading result + next question (or completion). | JWT | +| `GET` | `/api/diagnostic//result` | Get diagnostic result (proficiency profile). | JWT | +| `POST` | `/api/diagnostic/retake` | Retake diagnostic for a subject. Body: `{ "subjectId": 1 }`. | JWT | + +### 10.4 Proficiency Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/proficiency` | Get current student's proficiency profile. Query: `?subjectId=1`. | JWT | +| `GET` | `/api/proficiency/summary` | Get mastery summary across all subjects. | JWT | +| `GET` | `/api/proficiency/class` | Get class-level proficiency (teacher/admin). Query: `?subjectId=1&classroomId=5`. | JWT (teacher/admin) | + +### 10.5 Learning Plan Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/learning-plan` | Get current student's active plan. Query: `?subjectId=1`. | JWT | +| `POST` | `/api/learning-plan/generate` | Generate or regenerate plan. Body: `{ "subjectId": 1, "targetDate": "2026-06-15" }`. | JWT | +| `PATCH` | `/api/learning-plan/` | Update plan (teacher override). Body: `{ "items": [...] }`. | JWT (teacher) | +| `POST` | `/api/learning-plan//pause` | Pause plan. | JWT | +| `POST` | `/api/learning-plan//resume` | Resume plan. | JWT | +| `GET` | `/api/learning-plan//progress` | Get detailed plan progress. | JWT | + +### 10.6 Content and Coaching Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/topics//content` | Get learning content for a topic (human resources + AI-generated, resolved by priority). | JWT | +| `POST` | `/api/topics//generate-content` | Force AI content generation for a topic. Body: `{ "type": "explanation", "difficultyLevel": "intermediate" }`. | JWT (staff) | +| `POST` | `/api/topics//practice` | Generate practice questions. Body: `{ "count": 5, "questionTypes": ["multiple_choice", "numerical"] }`. Returns questions with IDs. | JWT | +| `POST` | `/api/topics//practice/grade` | Submit practice answers for grading. Body: `{ "answers": [{"questionId": "...", "answer": "..."}] }`. Returns grading results with explanations. | JWT | +| `POST` | `/api/topics//mastery-quiz` | Start mastery quiz. Returns question set. | JWT | +| `POST` | `/api/topics//mastery-quiz/submit` | Submit mastery quiz. Body: `{ "answers": [...] }`. Returns score, pass/fail, updated mastery. | JWT | +| `POST` | `/api/coach/hint` | Request a hint. Body: `{ "questionId": "...", "topicId": 105, "currentAnswer": "..." }`. Returns hint text. | JWT | +| `POST` | `/api/coach/explain` | Request explanation. Body: `{ "questionId": "...", "topicId": 105, "studentAnswer": "...", "correctAnswer": "..." }`. Returns detailed explanation. | JWT | +| `POST` | `/api/coach/suggest` | Get study suggestions. Body: `{ "subjectId": 1 }`. Returns personalized study advice. | JWT | + +### 10.7 Analytics Endpoints + +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| `GET` | `/api/analytics/student` | Student's own analytics. Query: `?subjectId=1`. | JWT | +| `GET` | `/api/analytics/class` | Class-level analytics (teacher/admin). Query: `?subjectId=1&classroomId=5`. | JWT (teacher/admin) | +| `GET` | `/api/analytics/subject` | Subject-level analytics (admin). Query: `?subjectId=1`. | JWT (admin) | +| `GET` | `/api/analytics/content-gaps` | Content gap report (admin/staff). Query: `?subjectId=1`. | JWT (admin/staff) | + +--- + +## 11. AI Integration Specification + +### 11.1 New Odoo Module: `encoach_adaptive_ai` + +This module provides AI services for the adaptive learning system. It follows the same patterns as `encoach_ai_generation` and `encoach_ai_grading`: a service class that calls `EncoachOpenAIService` with subject-specific prompts. + +### 11.2 GPT-4o Prompt Templates + +#### 11.2.1 Diagnostic Question Generation + +**Math Example:** + +``` +System: You are a mathematics assessment expert. Generate an adaptive diagnostic question. + +Given: +- Subject: Mathematics +- Domain: {domain_name} +- Topic: {topic_name} +- Difficulty: {difficulty_level} (easy/medium/hard/advanced) +- Question type: {question_type} +- Learning objectives: {objectives_list} + +Generate a single question with: +1. "question": The question text (use LaTeX notation for formulas: $...$) +2. "options": Array of 4 options (for multiple_choice only) +3. "correct_answer": The correct answer +4. "explanation": Brief explanation of why the answer is correct +5. "grading_rubric": How to grade (for open-ended: key concepts to look for) +6. "difficulty_tag": The actual difficulty of the generated question + +Respond in JSON format only. +``` + +**IT Example:** + +``` +System: You are an Information Technology assessment expert. Generate an adaptive diagnostic question. + +Given: +- Subject: Information Technology +- Domain: {domain_name} +- Topic: {topic_name} +- Difficulty: {difficulty_level} +- Question type: {question_type} +- Learning objectives: {objectives_list} + +Generate a single question with: +1. "question": The question text (use ```code blocks``` for code snippets) +2. "options": Array of 4 options (for multiple_choice/true_false) +3. "correct_answer": The correct answer +4. "explanation": Brief explanation +5. "grading_rubric": How to grade +6. "difficulty_tag": Actual difficulty + +For code_completion type, include: +- "starter_code": Code with blanks/comments for student to complete +- "expected_output": What correct code should produce +- "evaluation_criteria": ["correctness", "approach", "efficiency"] + +Respond in JSON format only. +``` + +#### 11.2.2 Learning Plan Generation + +``` +System: You are an adaptive learning plan advisor. Create a personalized study plan. + +Given: +- Student proficiency profile: + {proficiency_json} +- Subject taxonomy with prerequisites: + {taxonomy_json} +- Target completion date: {target_date} +- Mastery threshold: {mastery_threshold}% + +Generate a study plan with: +1. "summary": A 2-3 sentence motivational overview of the plan, acknowledging strengths and addressing weaknesses +2. "recommended_sequence": Ordered list of topic IDs to study, respecting prerequisites +3. "estimated_hours_per_topic": Object mapping topic_id to estimated study hours (adjusted by current mastery) +4. "milestones": Array of milestone objects with {"name", "topic_ids", "target_date", "description"} +5. "focus_areas": Top 3 domains/topics that need the most attention +6. "study_advice": Specific tips for the student based on their profile + +Respond in JSON format only. +``` + +#### 11.2.3 Content Generation + +``` +System: You are an expert {subject_name} tutor. Generate learning content for the following topic. + +Given: +- Subject: {subject_name} +- Topic: {topic_name} +- Learning objectives: {objectives_list} +- Student mastery level: {mastery_level} ({mastery_percentage}%) +- Content type: {content_type} (explanation/worked_example/key_takeaways) +- Available context: {resource_summaries} (summaries of human-uploaded resources, if any) + +For "explanation": +Generate a clear, structured explanation of {topic_name} that: +- Starts from the student's current level ({mastery_level}) +- Covers all learning objectives +- Uses concrete examples +- For Math: uses LaTeX notation ($...$) for formulas +- For IT: uses ```language``` code blocks for code examples +- Ends with a brief summary + +For "worked_example": +Generate 2-3 step-by-step worked examples that: +- Progress from simple to complex +- Show every intermediate step +- Highlight common mistakes to avoid +- For Math: show formula transformations step by step +- For IT: show code evolution or decision reasoning + +For "key_takeaways": +Generate 3-5 bullet points summarizing the most important concepts, formulas, or patterns from this topic. + +Respond in Markdown format. +``` + +#### 11.2.4 AI Coaching -- Hint + +``` +System: You are a helpful {subject_name} tutor providing a hint to a student. + +The student is working on this question: +{question_text} + +Their current (incomplete or incorrect) answer: {student_answer} +The correct answer: {correct_answer} + +Provide a hint that: +- Does NOT reveal the full answer +- Points the student toward the right approach +- References relevant concepts or formulas +- Is encouraging and supportive +- Is 1-3 sentences long + +Respond with just the hint text. +``` + +#### 11.2.5 AI Coaching -- Explanation + +``` +System: You are a {subject_name} tutor explaining a question result. + +Question: {question_text} +Student's answer: {student_answer} +Correct answer: {correct_answer} +Was correct: {is_correct} + +Provide an explanation that: +- Acknowledges whether the student was correct or incorrect +- Explains WHY the correct answer is correct +- If the student was wrong, explains the specific misconception +- References the relevant concept/formula/principle +- Suggests what to review if the student was wrong +- For Math: shows the solution steps +- For IT: explains the underlying principle + +Respond in Markdown format (2-4 paragraphs). +``` + +### 11.3 Grading Logic per Question Type + +| Question Type | Grading Method | Implementation | +|--------------|---------------|----------------| +| `multiple_choice` | Exact match | Direct comparison: `student_answer == correct_answer` | +| `true_false` | Exact match | Direct comparison | +| `numerical` | Tolerance-based | `abs(student - correct) <= tolerance`. Default tolerance: 0.01. Configurable per topic. | +| `fill_blanks` | Normalized match | Strip whitespace, lowercase, compare. For Math: normalize LaTeX expressions. | +| `short_answer` | AI-graded | GPT-4o evaluates semantic equivalence. Prompt includes correct answer and acceptable variations. | +| `code_completion` | AI-graded | GPT-4o evaluates: (1) correctness of output, (2) approach validity, (3) code quality. Returns score 0-100 + feedback. | +| `worked_problem` | AI-graded | GPT-4o evaluates: (1) final answer correctness, (2) methodology, (3) step completeness. Returns score 0-100 + per-step feedback. | +| `scenario` | AI-graded | GPT-4o evaluates against rubric provided with the question. Returns score 0-100 + explanation. | + +### 11.4 FAISS Extension + +Create subject-specific FAISS indices for training tips: + +| Index | Category | Content Source | +|-------|----------|---------------| +| `math_algebra_tips` | Algebra tips and common mistakes | Curated + AI-generated | +| `math_geometry_tips` | Geometry tips and formulas | Curated + AI-generated | +| `math_statistics_tips` | Statistics tips and methods | Curated + AI-generated | +| `it_networking_tips` | Networking concepts and troubleshooting | Curated + AI-generated | +| `it_database_tips` | SQL tips and database design patterns | Curated + AI-generated | +| `it_programming_tips` | Coding tips and best practices | Curated + AI-generated | + +Each index uses `all-MiniLM-L6-v2` for embeddings (same as English), stored as `encoach.training.tip` records with a new `subject_id` field. + +--- + +## 12. Frontend Requirements + +### 12.1 New Pages + +| Page | Route | Role | Description | +|------|-------|------|-------------| +| **Subject Selection** | `/student/subjects` | Student | List available subjects, show overall mastery per subject, "Start Learning" button | +| **Diagnostic Test** | `/student/diagnostic/:subjectId` | Student | Adaptive test UI: one question at a time, progress indicator, timer, auto-advance on answer | +| **Proficiency Profile** | `/student/proficiency/:subjectId` | Student | Visual mastery map: domain radar chart, per-topic mastery bars, strengths/weaknesses | +| **Learning Plan** | `/student/plan/:subjectId` | Student | Topic sequence with status indicators (locked/available/in-progress/completed), progress bar, timeline | +| **Topic Learning** | `/student/topic/:topicId` | Student | Content viewer: resources list, AI content, practice section, mastery quiz trigger | +| **Practice Session** | `/student/practice/:topicId` | Student | Question-by-question practice with instant feedback, hints, explanations | +| **Mastery Quiz** | `/student/mastery-quiz/:topicId` | Student | Timed quiz, similar to existing exam UI, results with pass/fail and mastery update | +| **AI Coach** | Component (sidebar/modal) | Student | Contextual AI assistant: hint requests, explanations, study suggestions | +| **Taxonomy Manager** | `/admin/taxonomy` | Admin/Staff | CRUD for subjects, domains, topics, objectives, prerequisite mapping | +| **Resource Manager** | `/admin/resources` | Admin/Staff | Upload, tag, manage resources; content gap dashboard | +| **Adaptive Analytics** | `/admin/adaptive-analytics` | Admin | Subject-level analytics, class performance, content gaps | +| **Student Plan View** | `/teacher/student-plan/:studentId/:subjectId` | Teacher | View and override a student's learning plan | + +### 12.2 Math-Specific Components + +| Component | Technology | Purpose | +|-----------|-----------|---------| +| `MathRenderer` | KaTeX (via `react-katex` or `remark-math` + `rehype-katex`) | Render LaTeX formulas in questions, explanations, and content. Inline: `$x^2$`, block: `$$\frac{a}{b}$$` | +| `MathInput` | `mathlive` or custom LaTeX input | Student enters mathematical expressions as answers. Visual equation editor with keyboard shortcuts. | +| `FormulaReference` | KaTeX | Sidebar panel showing relevant formulas for the current topic | +| `GraphPlot` | `recharts` or `plotly.js` | Display mathematical graphs (for coordinate geometry, functions) | + +### 12.3 IT-Specific Components + +| Component | Technology | Purpose | +|-----------|-----------|---------| +| `CodeBlock` | `prism-react-renderer` or `react-syntax-highlighter` | Render code snippets with syntax highlighting in questions and content | +| `CodeEditor` | `@monaco-editor/react` (lightweight) or `codemirror` | Student writes/edits code for code_completion questions | +| `TerminalOutput` | Custom styled `
` | Display expected program output |
+| `NetworkDiagram` | `react-flow` or Mermaid | Display network topology diagrams for networking questions |
+
+### 12.4 Shared Components
+
+| Component | Purpose |
+|-----------|---------|
+| `MasteryBar` | Horizontal bar showing mastery level with color coding (grey/red/orange/yellow/green) |
+| `DomainRadar` | Radar chart showing mastery across all domains in a subject |
+| `TopicCard` | Card showing topic name, mastery, status (locked/available/completed), prerequisite indicator |
+| `PlanTimeline` | Vertical timeline of learning plan items with status icons |
+| `QuizTimer` | Countdown timer for mastery quizzes (reuse from existing exam timer) |
+| `ProgressRing` | Circular progress indicator for overall subject mastery |
+| `ResourceCard` | Card for learning resources with type icon, title, estimated time, completion checkbox |
+| `CoachBubble` | Chat-like bubble for AI coaching responses |
+
+---
+
+## 13. Non-Functional Requirements
+
+### 13.1 Performance
+
+| Metric | Target |
+|--------|--------|
+| Diagnostic question generation | < 3 seconds per question |
+| Practice question generation (batch of 5) | < 5 seconds |
+| AI grading (per answer) | < 5 seconds for auto-gradeable, < 10 seconds for AI-graded |
+| Learning plan generation | < 10 seconds |
+| AI content generation (explanation) | < 15 seconds |
+| AI coaching response (hint/explanation) | < 5 seconds |
+| Resource file upload | < 30 seconds for files up to 50 MB |
+| Proficiency profile load | < 1 second |
+| Taxonomy tree load | < 2 seconds |
+
+### 13.2 Scalability
+
+| Dimension | Target |
+|-----------|--------|
+| Concurrent students per subject | 200 |
+| Total topics per subject | 500 |
+| Total resources per subject | 1,000 |
+| AI content cache entries | 10,000 |
+| Proficiency records | 200 students x 500 topics = 100,000 records |
+
+### 13.3 Data Integrity
+
+- Proficiency scores are never overwritten -- all changes are additive (new assessment results update the score via weighted average, never direct replacement)
+- Learning plan changes are logged (who changed, when, what was before/after)
+- AI-generated content is cached and versioned; regeneration creates a new version, does not delete the old one
+- Resource deletions are soft-deletes (is_active = False) to preserve student completion records
+
+### 13.4 Security
+
+- All endpoints require JWT authentication (via existing `_authenticate()`)
+- Students can only access their own proficiency and learning plan data
+- Teachers can view (but not modify without explicit override) student data in their classrooms
+- Admin has full access
+- Resource uploads are scanned for file type validation (no executable uploads)
+- AI prompts never include student personal data (only anonymized performance metrics)
+
+---
+
+## Appendix A: Implementation Priority
+
+| Priority | Component | Rationale |
+|----------|-----------|-----------|
+| **P0** | Subject taxonomy models + CRUD API | Foundation for everything else |
+| **P0** | Resource model + upload API | Staff needs to start uploading content immediately |
+| **P1** | Diagnostic assessment engine + UI | Entry point for students |
+| **P1** | Proficiency profile model + API | Required by learning plan |
+| **P1** | Learning plan generation + UI | Core student experience |
+| **P2** | Content delivery (resource viewer + AI content) | Content consumption |
+| **P2** | Practice questions + grading | Practice before mastery quiz |
+| **P2** | Mastery quiz + progression | Advancement mechanism |
+| **P3** | AI coaching (hints, explanations) | Enhancement to learning experience |
+| **P3** | Spaced repetition | Long-term retention |
+| **P3** | Analytics dashboards | Monitoring and optimization |
+| **P4** | Content gap detection | Operational efficiency |
+| **P4** | FAISS training tips per subject | Advanced personalization |
+
+## Appendix B: Dependency Graph for Implementation
+
+```mermaid
+graph TB
+    TaxModels["P0: Taxonomy Models
(Subject, Domain, Topic, Objective)"] --> TaxAPI["P0: Taxonomy CRUD API"] + TaxModels --> ResModels["P0: Resource Model"] + ResModels --> ResAPI["P0: Resource Upload API"] + + TaxAPI --> DiagEngine["P1: Diagnostic Engine"] + TaxModels --> ProfModel["P1: Proficiency Model"] + DiagEngine --> ProfModel + ProfModel --> PlanGen["P1: Learning Plan Generation"] + TaxModels --> PlanGen + + ResAPI --> ContentDel["P2: Content Delivery"] + PlanGen --> ContentDel + ContentDel --> Practice["P2: Practice Questions"] + Practice --> MasteryQuiz["P2: Mastery Quiz"] + MasteryQuiz --> ProfModel + + Practice --> Coaching["P3: AI Coaching"] + MasteryQuiz --> SpacedRep["P3: Spaced Repetition"] + ProfModel --> Analytics["P3: Analytics"] + + ResAPI --> GapDetect["P4: Content Gap Detection"] + ProfModel --> FAISSTips["P4: FAISS Tips per Subject"] +``` + +--- + +*Document prepared for architect review. All data models, APIs, and specifications are draft and subject to refinement before handoff to the development team.* diff --git a/docs/ODOO_BACKEND_SRS_v3.md b/docs/ODOO_BACKEND_SRS_v3.md new file mode 100644 index 00000000..5073afbf --- /dev/null +++ b/docs/ODOO_BACKEND_SRS_v3.md @@ -0,0 +1,749 @@ +# EnCoach Odoo 19 Backend -- Developer SRS v3 + +> **SUPERSEDED** -- This document has been replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` (v3.0) and `ENCOACH_UNIFIED_SRS.md` (v2.0). All content below is historical. The developer has implemented everything and the system is deployed at `http://5.189.151.117:8069`. + +**Document Version:** 3.0 +**Date:** March 11, 2026 +**Status:** ~~Active -- Ready for Development~~ **SUPERSEDED** +**Supersedes:** `ODOO_MIGRATION_SRS_v2.md` (v2.0) +**Master Reference:** `ENCOACH_UNIFIED_SRS.md` (v1.0) +**Author:** EnCoach Architecture Team + +--- + +## Purpose + +This document is the **backend implementation specification** for the Odoo 19 developer. It defines every API endpoint, data model, and module the frontend expects. The frontend is **fully built and wired** -- it makes real HTTP calls to these endpoints via TanStack Query. Your job is to ensure every endpoint listed here exists, returns the correct response shape, and connects to the correct Odoo models. + +**Source of truth for response shapes:** `encoach_frontend_new/src/types/*.ts` (14 files) +**Source of truth for endpoint paths:** `encoach_frontend_new/src/services/*.ts` (21 files) + +--- + +## Table of Contents + +1. [What Already Exists](#1-what-already-exists) +2. [What Needs to Be Built or Modified](#2-what-needs-to-be-built-or-modified) +3. [New Odoo Modules](#3-new-odoo-modules) +4. [Existing Module Modifications](#4-existing-module-modifications) +5. [Complete API Contract](#5-complete-api-contract) +6. [Data Model Specification](#6-data-model-specification) +7. [AI Service Integration](#7-ai-service-integration) +8. [Response Format Standards](#8-response-format-standards) +9. [Non-Functional Requirements](#9-non-functional-requirements) +10. [Implementation Priority](#10-implementation-priority) + +--- + +## 1. What Already Exists + +### 1.1 Existing Odoo Modules (15) + +These modules are built and deployed in `ielts-be-v0/`: + +| # | Module | Controller File | Endpoints | +|---|--------|----------------|-----------| +| 1 | `encoach_core` | -- | Users, entities, roles, permissions, codes, invites | +| 2 | `encoach_ai` | -- | OpenAI service wrapper, constants, blacklist | +| 3 | `encoach_ai_media` | `media.py` | TTS (Polly), STT (Whisper), ELAI avatars | +| 4 | `encoach_ai_generation` | `generation.py` | Exam content generation per module | +| 5 | `encoach_ai_grading` | `grading.py` | Writing/speaking AI grading | +| 6 | `encoach_exam` | `exams.py` | Exam CRUD, approval workflows | +| 7 | `encoach_assignment` | `assignments.py` | Assignment management | +| 8 | `encoach_evaluation` | `evaluations.py` | Evaluation results and AI jobs | +| 9 | `encoach_stats` | `sessions.py`, `stats.py` | Exam sessions and statistics | +| 10 | `encoach_training` | `training.py` | Training content, tips (FAISS), walkthroughs | +| 11 | `encoach_classroom` | `classrooms.py` | Classroom groups | +| 12 | `encoach_subscription` | `subscriptions.py`, `discounts.py` | Packages, payments, discounts | +| 13 | `encoach_registration` | `registration.py` | User registration, batch import | +| 14 | `encoach_ticket` | `tickets.py` | Support tickets | +| 15 | `encoach_api` | `auth.py`, `users.py`, `entities.py`, `storage.py`, `approvals.py` | REST API controllers (81+ endpoints) | + +### 1.2 Existing Endpoint Count by Group + +| Group | Estimated Count | Status | +|-------|----------------|--------| +| Auth (`/api/login`, `/api/logout`, etc.) | 4 | Built | +| Users (`/api/user`, `/api/users/*`) | 6 | Built | +| Entities & Roles (`/api/entities`, `/api/roles`, `/api/permissions`) | 7 | Built | +| Exams (`/api/exam/*`) | 7 | Built | +| AI Generation (`/api/exam/*/generate`) | 12 | Built | +| Media (`/api/exam/*/media`, `/api/transcribe`) | 6 | Built | +| Evaluations (`/api/evaluate/*`, `/api/grading/*`) | 6 | Built | +| Classrooms (`/api/groups`) | 3 | Built | +| Assignments (`/api/assignments`) | 8 | Built | +| Sessions & Stats (`/api/sessions`, `/api/stats`, `/api/statistical`) | 5 | Built | +| Training (`/api/training/*`) | 4 | Built | +| Subscriptions (`/api/packages`, `/api/stripe`, etc.) | 7 | Built | +| Registration (`/api/register`, `/api/code/*`) | 4 | Built | +| Tickets (`/api/tickets`) | 3 | Built | +| Storage (`/api/storage`) | 2 | Built | +| Approvals (`/api/approval-workflows`) | 3 | Built | +| **TOTAL EXISTING** | **~87** | | + +--- + +## 2. What Needs to Be Built or Modified + +### 2.1 New Endpoints Required (Frontend Expects These) + +The frontend calls the following endpoints that **do not exist yet**. You must create them. + +| Group | Count | Priority | +|-------|-------|----------| +| Taxonomy (subjects, domains, topics) | 14 | P0 | +| Adaptive Learning (diagnostic, proficiency, learning plans) | 16 | P0 | +| Resources (upload, manage, review) | 7 | P1 | +| AI Coaching (chat, suggest, explain, tips) | 6 | P1 | +| AI Utilities (search, insights, alerts, reports) | 5 | P2 | +| Analytics (student, class, subject, content gaps) | 4 | P2 | +| LMS Bridge (courses, batches, timetable, attendance, grades) | 13 | P1 | +| **TOTAL NEW** | **~65** | | + +### 2.2 Existing Modules Requiring Modification + +| Module | What to Change | +|--------|---------------| +| `encoach_exam` | Add `subject_id` (Many2one to `encoach.subject`) and `topic_ids` (Many2many to `encoach.topic`) fields to `encoach.exam` model | +| `encoach_stats` | Add `topic_id` and `subject_id` fields to `encoach.stat` model | +| `encoach_training` | Add `subject_id` field to `encoach.training.tip` for per-subject FAISS indices | +| `encoach_ai_generation` | Extend prompts to support Math (include LaTeX/KaTeX formatting) and IT (code blocks, syntax) question types | +| `encoach_ai_grading` | Add grading logic for numerical-tolerance (Math) and keyword/pattern (IT) answers | +| `encoach_subscription` | Add `subjects` field to `encoach.package` for subject-scoped subscriptions | +| `encoach_api` | Add new controller files for all new endpoint groups | + +--- + +## 3. New Odoo Modules + +Create these 8 new modules: + +### 3.1 `encoach_taxonomy` + +**Purpose:** Subject/domain/topic/learning-objective hierarchy. + +**Models:** + +| Model | Key Fields | +|-------|-----------| +| `encoach.subject` | `name` (Char), `code` (Char unique), `is_active` (Boolean), `diagnostic_config` (Text/JSON), `mastery_threshold` (Float default=80), `grading_scale` (Selection: percentage/band/letter), `grading_scale_config` (Text/JSON) | +| `encoach.domain` | `name` (Char), `code` (Char), `subject_id` (M2O encoach.subject), `sequence` (Integer) | +| `encoach.topic` | `name` (Char), `code` (Char), `domain_id` (M2O encoach.domain), `estimated_hours` (Float), `difficulty_level` (Selection: easy/medium/hard), `prerequisite_ids` (M2M self-ref), `question_type_weights` (Text/JSON) | +| `encoach.learning.objective` | `name` (Char), `topic_id` (M2O encoach.topic), `bloom_level` (Selection: remember/understand/apply/analyze/evaluate/create), `sequence` (Integer) | + +**Computed fields on `encoach.subject`:** `domain_count`, `topic_count` +**Computed fields on `encoach.domain`:** `topic_count` +**Computed fields on `encoach.topic`:** `objective_count`, `prerequisite_names` (list of names) + +### 3.2 `encoach_resources` + +**Purpose:** Human-uploaded learning materials tagged to topics. + +**Models:** + +| Model | Key Fields | +|-------|-----------| +| `encoach.resource` | `name` (Char), `resource_type` (Selection: pdf/video/link/document/interactive), `url` (Char), `file` (Binary), `file_name` (Char), `topic_ids` (M2M encoach.topic), `author_id` (M2O res.users), `is_published` (Boolean), `review_status` (Selection: pending/approved/rejected) | +| `encoach.resource.completion` | `student_id` (M2O res.users), `resource_id` (M2O encoach.resource), `viewed` (Boolean), `time_spent_minutes` (Integer), `rating` (Integer 1-5) | + +### 3.3 `encoach_adaptive` + +**Purpose:** Core adaptive learning engine -- proficiency tracking, learning plans, content cache. + +**Models:** + +| Model | Key Fields | +|-------|-----------| +| `encoach.proficiency` | `student_id` (M2O res.users), `topic_id` (M2O encoach.topic), `mastery` (Float 0-100), `mastery_level` (Selection: not_started/beginner/developing/proficient/mastered -- computed from mastery), `last_assessed` (Datetime), `time_spent_minutes` (Integer) | +| `encoach.learning.plan` | `student_id` (M2O res.users), `subject_id` (M2O encoach.subject), `status` (Selection: active/paused/completed), `ai_summary` (Text), `overall_progress` (Float -- computed), `target_completion` (Date) | +| `encoach.learning.plan.item` | `plan_id` (M2O encoach.learning.plan), `topic_id` (M2O encoach.topic), `sequence` (Integer), `status` (Selection: locked/available/in_progress/completed), `estimated_hours` (Float), `actual_hours` (Float), `mastery` (Float -- from proficiency) | +| `encoach.ai.content.cache` | `topic_id` (M2O encoach.topic), `content_type` (Char), `difficulty_level` (Char), `content` (Text/JSON), `model_used` (Char), `review_status` (Selection: auto/reviewed/rejected) | + +**Business logic:** +- `mastery_level` is computed: 0-19 = not_started, 20-39 = beginner, 40-59 = developing, 60-79 = proficient, 80-100 = mastered +- `overall_progress` is computed from item statuses (completed_count / total_count * 100) +- When a plan item is completed, unlock the next item(s) based on prerequisite graph + +### 3.4 `encoach_adaptive_api` + +**Purpose:** REST controllers for the adaptive learning engine. + +**Controller files to create:** +- `taxonomy.py` -- `/api/subjects`, `/api/domains`, `/api/topics`, `/api/subjects/{id}/taxonomy` +- `resources.py` -- `/api/resources` +- `diagnostic.py` -- `/api/diagnostic/start`, `/api/diagnostic/answer`, `/api/diagnostic/{id}/result` +- `proficiency.py` -- `/api/proficiency`, `/api/proficiency/summary`, `/api/proficiency/class` +- `learning_plan.py` -- `/api/learning-plan`, `/api/learning-plan/generate`, etc. +- `content.py` -- `/api/topics/{id}/content`, `/api/topics/{id}/practice`, `/api/topics/{id}/mastery-quiz` + +### 3.5 `encoach_adaptive_ai` + +**Purpose:** AI logic for diagnostics, plan generation, content generation, coaching. + +**Key services:** +- Diagnostic question selection (adaptive difficulty algorithm) +- Learning plan generation (topological sort of topics by prerequisites, weighted by proficiency gaps) +- AI content generation for topics (call OpenAI GPT-4o with topic context) +- Practice question generation per topic +- Mastery quiz generation and grading +- Coaching: chat, hints, explanations, study suggestions, writing help, contextual tips + +### 3.6 `encoach_lms_api` + +**Purpose:** REST API bridge exposing OpenEduCat models to the frontend. + +**Controller files to create:** +- `courses.py` -- `/api/courses`, `/api/courses/{id}`, `/api/courses/ai-generate` +- `batches.py` -- `/api/batches`, `/api/batches/{id}` +- `timetable.py` -- `/api/timetable` +- `attendance.py` -- `/api/attendance` +- `grades.py` -- `/api/grades` + +**Note:** These controllers wrap the existing OpenEduCat Odoo models (`op.course`, `op.batch`, `op.session`, `op.attendance.sheet`, etc.) behind a clean REST API. Do NOT duplicate the models -- use the OpenEduCat models directly. + +### 3.7 `encoach_sis` + +**Purpose:** UTAS Student Information System integration. + +**Models:** +- `encoach.sis.sync` -- sync job tracking (status, last_run, next_run, error_log) +- `encoach.sis.mapping` -- field mapping between SIS fields and EnCoach fields + +### 3.8 `encoach_branding` + +**Purpose:** Tenant whitelabeling. + +**Models:** +- `encoach.branding` -- `entity_id` (M2O encoach.entity), `logo` (Binary), `primary_color` (Char), `secondary_color` (Char), `font_family` (Char) + +--- + +## 4. Existing Module Modifications + +### 4.1 `encoach_exam` -- Add Subject/Topic Fields + +```python +# In encoach_exam/models/exam.py, add: +subject_id = fields.Many2one('encoach.subject', string='Subject') +topic_ids = fields.Many2many('encoach.topic', string='Topics') +is_diagnostic = fields.Boolean(string='Is Diagnostic Exam', default=False) +``` + +### 4.2 `encoach_stats` -- Add Subject/Topic Fields + +```python +# In encoach_stats/models/stat.py, add: +subject_id = fields.Many2one('encoach.subject', string='Subject') +topic_id = fields.Many2one('encoach.topic', string='Topic') +``` + +### 4.3 `encoach_training` -- Add Subject Field + +```python +# In encoach_training/models/training_tip.py, add: +subject_id = fields.Many2one('encoach.subject', string='Subject') +``` + +### 4.4 `encoach_subscription` -- Add Subject Scoping + +```python +# In encoach_subscription/models/package.py, add: +subject_ids = fields.Many2many('encoach.subject', string='Included Subjects') +``` + +### 4.5 `encoach_ai_generation` -- Multi-Subject Prompts + +Extend the generation prompts to handle: +- **Math:** Include LaTeX formatting instructions in prompts, numerical-tolerance answer checking +- **IT:** Include code block formatting, syntax highlighting hints, code-completion question types +- **Generic:** Accept `subject_id` and `topic_id` parameters, adjust prompt templates per subject + +### 4.6 `encoach_ai_grading` -- Multi-Subject Grading + +Extend grading logic: +- **IELTS:** Band scoring (existing) +- **Math:** Numerical tolerance (accept answers within +/- epsilon), step-by-step partial credit +- **IT:** Keyword matching, regex pattern matching, AI-graded code explanations + +--- + +## 5. Complete API Contract + +This is the definitive list of every endpoint the frontend calls. Each entry shows the HTTP method, path, request body (if POST/PATCH), and expected response shape. **The TypeScript type files in `encoach_frontend_new/src/types/` are the canonical response shapes.** + +### 5.1 Authentication (Existing -- `auth.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `POST` | `/api/login` | `{ login: string, password: string }` | `{ token: string, user: User }` | +| `POST` | `/api/logout` | -- | `void` | +| `GET` | `/api/user` | -- | `User` | +| `POST` | `/api/reset/sendVerification` | `{ email: string }` | `{ success: boolean }` | + +**User shape:** See `src/types/auth.ts` -- must include: `id`, `name`, `email`, `login`, `user_type` (one of: student/teacher/admin/corporate/mastercorporate/agent/developer), `is_verified`, `entities` (array of {id, name, role}), `classrooms`. + +### 5.2 Users (Existing -- `users.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `GET` | `/api/users/list` | Query: `type`, `entity_id`, `page`, `size`, `search`, `sort`, `order` | `PaginatedResponse` | +| `GET` | `/api/users/{id}` | -- | `User` | +| `PATCH` | `/api/users/update` | `{ id, ...fields }` | `User` | +| `POST` | `/api/users/create` | `Partial` | `User` | +| `POST` | `/api/batch_users` | `{ users: Partial[] }` | `{ success: boolean }` | + +**PaginatedResponse shape:** `{ items: T[], total: number, page: number, size: number, pages: number }` + +### 5.3 Entities (Existing -- `entities.service.ts`) + +| Method | Path | Response | +|--------|------|----------| +| `GET` | `/api/entities` | `PaginatedResponse` | +| `GET` | `/api/entities/{id}` | `Entity` | +| `POST` | `/api/entities` | `Entity` | +| `PATCH` | `/api/entities/{id}` | `Entity` | +| `DELETE` | `/api/entities/{id}` | `{ success: boolean }` | +| `GET` | `/api/entities/{id}/roles` | `EntityRole[]` | +| `POST` | `/api/entities/{id}/roles` | `EntityRole` | +| `PATCH` | `/api/roles/{id}/permissions` | `EntityRole` | +| `GET` | `/api/permissions?entity_id=` | `string[]` (list of permission keys for current user) | + +### 5.4 Exams (Existing -- `exams.service.ts`) + +| Method | Path | Response | +|--------|------|----------| +| `GET` | `/api/exam/{module}` | `PaginatedResponse` | +| `GET` | `/api/exam/{module}/{id}` | `Exam` | +| `POST` | `/api/exam` | `Exam` | +| `PATCH` | `/api/exam/{id}` | `Exam` | +| `DELETE` | `/api/exam/{id}` | `{ success: boolean }` | +| `PATCH` | `/api/exam/{id}/access` | `Exam` | +| `GET` | `/api/rubrics` | `PaginatedResponse` | +| `POST` | `/api/rubrics` | `Rubric` | +| `GET` | `/api/rubric-groups` | `PaginatedResponse` | +| `GET` | `/api/exam-structures` | `PaginatedResponse` | +| `POST` | `/api/exam-structures` | `ExamStructure` | +| `DELETE` | `/api/exam-structures/{id}` | `{ success: boolean }` | +| `GET` | `/api/exam/avatars` | `[{ id, name, thumbnail, voice }]` | + +**Note:** `Exam` response must now include optional `subject_id` and `topic_ids` fields. + +### 5.5 Assignments (Existing -- `assignments.service.ts`) + +| Method | Path | Response | +|--------|------|----------| +| `GET` | `/api/assignments` | `PaginatedResponse` | +| `GET` | `/api/assignments/{id}` | `Assignment` | +| `POST` | `/api/assignments` | `Assignment` | +| `PATCH` | `/api/assignments/{id}` | `Assignment` | +| `DELETE` | `/api/assignments/{id}` | `{ success: boolean }` | +| `POST` | `/api/assignments/{id}/archive` | `Assignment` | +| `POST` | `/api/assignments/{id}/start` | `Assignment` | + +### 5.6 Classrooms (Existing -- `classrooms.service.ts`) + +| Method | Path | Response | +|--------|------|----------| +| `GET` | `/api/groups` | `PaginatedResponse` | +| `GET` | `/api/groups/{id}` | `Classroom` | +| `POST` | `/api/groups` | `Classroom` | +| `DELETE` | `/api/groups/{id}` | `{ success: boolean }` | +| `POST` | `/api/groups/transfer` | `{ success: boolean }` | +| `POST` | `/api/groups/{id}/members` | `{ success: boolean }` | +| `POST` | `/api/groups/{id}/members/remove` | `{ success: boolean }` | + +### 5.7 Stats (Existing -- `stats.service.ts`) + +| Method | Path | Response | +|--------|------|----------| +| `GET` | `/api/sessions` | `ExamSession[]` | +| `GET` | `/api/stats` | `ExamStat[]` | +| `GET` | `/api/statistical` | `StatisticalData` | +| `GET` | `/api/stats/performance` | `unknown[]` | + +### 5.8 Evaluations (Existing -- `evaluations.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `POST` | `/api/evaluate/writing` | `{ session_id, text, rubric_id? }` | `Evaluation` | +| `POST` | `/api/evaluate/speaking` | `{ session_id, audio_url, rubric_id? }` | `Evaluation` | +| `POST` | `/api/grading/multiple` | `{ session_id, answers: [{exercise_index, answer}] }` | `{ results: [{exercise_index, correct, score}] }` | +| `POST` | `/api/transcribe` | `FormData (audio file)` | `{ text: string }` | + +### 5.9 Generation (Existing -- `generation.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `POST` | `/api/exam/{module}/generate` | `{ title, label?, entity_id?, subject_id?, topic_id?, difficulty?, count? }` | `{ exam_id, exercises: [] }` | +| `POST` | `/api/exam/{module}/generate/scratch` | Same as above | Same as above | + +**Note:** Must now accept `subject_id` and `topic_id` to scope generation to specific subjects/topics. + +### 5.10 Training, Subscriptions, Tickets, Storage, Approvals (Existing) + +These follow the existing patterns. See `training.service.ts`, `subscriptions.service.ts`, `tickets.service.ts`, `storage.service.ts`, `approvals.service.ts` for exact paths. + +--- + +### 5.11 Taxonomy (NEW -- `taxonomy.service.ts`) + +| Method | Path | Request/Params | Response | +|--------|------|---------------|----------| +| `GET` | `/api/subjects` | -- | `Subject[]` | +| `GET` | `/api/subjects/{id}` | -- | `Subject` | +| `POST` | `/api/subjects` | `Partial` | `Subject` | +| `PATCH` | `/api/subjects/{id}` | `Partial` | `Subject` | +| `DELETE` | `/api/subjects/{id}` | -- | `{ success: boolean }` | +| `GET` | `/api/subjects/{id}/taxonomy` | -- | `TaxonomyTree` (nested: subject + domains + topics + objectives) | +| `POST` | `/api/subjects/{id}/taxonomy/import` | `FormData (CSV/JSON)` | `{ success: boolean }` | +| `GET` | `/api/domains` | Query: `subject_id?` | `Domain[]` | +| `POST` | `/api/domains` | `Partial` | `Domain` | +| `PATCH` | `/api/domains/{id}` | `Partial` | `Domain` | +| `DELETE` | `/api/domains/{id}` | -- | `{ success: boolean }` | +| `POST` | `/api/domains/{id}/ai-suggest` | -- | `{ suggestions: Partial[] }` | +| `GET` | `/api/topics` | Query: `domain_id?`, `subject_id?` | `Topic[]` | +| `POST` | `/api/topics` | `Partial` | `Topic` | +| `PATCH` | `/api/topics/{id}` | `Partial` | `Topic` | +| `DELETE` | `/api/topics/{id}` | -- | `{ success: boolean }` | + +**TaxonomyTree shape:** +```json +{ + "subject": { "id": 1, "name": "IELTS", "code": "IELTS", ... }, + "domains": [ + { + "id": 1, "name": "Writing", "code": "W", "sequence": 1, + "topics": [ + { + "id": 1, "name": "Task 1", "difficulty_level": "medium", + "objectives": [ + { "id": 1, "name": "Describe trends", "bloom_level": "apply" } + ] + } + ] + } + ] +} +``` + +### 5.12 Resources (NEW -- `resources.service.ts`) + +| Method | Path | Request/Params | Response | +|--------|------|---------------|----------| +| `GET` | `/api/resources` | Query: `topic_id?`, `resource_type?`, `review_status?`, `page`, `size`, `search` | `PaginatedResponse` | +| `POST` | `/api/resources` | `FormData` (file + metadata) | `Resource` | +| `PATCH` | `/api/resources/{id}` | `Partial` | `Resource` | +| `DELETE` | `/api/resources/{id}` | -- | `{ success: boolean }` | +| `POST` | `/api/resources/{id}/complete` | -- | `ResourceCompletion` | +| `POST` | `/api/resources/{id}/rate` | `{ rating: number }` | `{ success: boolean }` | +| `GET` | `/api/resources/{id}/download` | -- | Binary file (stream) | + +### 5.13 Diagnostic Assessment (NEW -- `adaptive.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `POST` | `/api/diagnostic/start` | `{ subject_id: number }` | `{ session: DiagnosticSession, first_question: DiagnosticQuestion }` | +| `POST` | `/api/diagnostic/answer` | `{ session_id, question_id, answer }` | `{ next_question?: DiagnosticQuestion, completed: boolean }` | +| `GET` | `/api/diagnostic/{id}/result` | -- | `DiagnosticResult` | + +**DiagnosticQuestion shape:** +```json +{ + "id": "q_uuid", + "topic_id": 5, + "topic_name": "Line Graphs", + "domain_name": "Writing", + "difficulty": "medium", + "question_type": "multiple_choice", + "question_text": "Which of the following...", + "options": ["A", "B", "C", "D"], + "time_limit_seconds": 120 +} +``` + +**Diagnostic algorithm:** +1. On `/start`: Create a diagnostic session. Pull topics from all domains. Start with `starting_difficulty` from subject's `diagnostic_config`. +2. On `/answer`: Grade the answer. If correct, increase difficulty and mastery estimate for that topic. If incorrect, decrease. Select the next question from a different domain/topic. Use Computer Adaptive Testing (CAT) principles. +3. Continue until `total_question_cap` reached or all domains have 2+ data points. +4. On completion: Write proficiency records for all assessed topics. + +### 5.14 Proficiency (NEW -- `adaptive.service.ts`) + +| Method | Path | Params | Response | +|--------|------|--------|----------| +| `GET` | `/api/proficiency` | Query: `subject_id?` | `Proficiency[]` | +| `GET` | `/api/proficiency/summary` | -- | `ProficiencySummary[]` | +| `GET` | `/api/proficiency/class` | Query: `subject_id?` | Class-level aggregation | + +**ProficiencySummary shape:** +```json +{ + "subject_id": 1, + "subject_name": "IELTS", + "overall_mastery": 65.5, + "domain_scores": [{ "domain_id": 1, "domain_name": "Writing", "mastery": 72 }], + "topics_mastered": 8, + "topics_total": 20 +} +``` + +### 5.15 Learning Plan (NEW -- `adaptive.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `GET` | `/api/learning-plan` | Query: `subject_id` | `LearningPlan` | +| `POST` | `/api/learning-plan/generate` | `{ subject_id, target_completion? }` | `LearningPlan` | +| `PATCH` | `/api/learning-plan/{id}` | `Partial` | `LearningPlan` | +| `POST` | `/api/learning-plan/{id}/pause` | -- | `LearningPlan` | +| `POST` | `/api/learning-plan/{id}/resume` | -- | `LearningPlan` | + +**Plan generation algorithm:** +1. Get student's proficiency records for the subject. +2. Identify topics below mastery threshold. +3. Topologically sort topics by prerequisites. +4. Create plan items in sequence. First item = available, rest = locked. +5. Call GPT-4o to generate `ai_summary` (natural language description of the plan). +6. Set `estimated_hours` from topic definitions. + +### 5.16 Content Delivery (NEW -- `adaptive.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `GET` | `/api/topics/{id}/content` | -- | `TopicContent` | +| `POST` | `/api/topics/{id}/generate-content` | -- | `TopicContent` | +| `POST` | `/api/topics/{id}/practice` | -- | `{ questions: [] }` | +| `POST` | `/api/topics/{id}/practice/grade` | `{ answers: [] }` | `{ results: [], mastery_update: number }` | +| `POST` | `/api/topics/{id}/mastery-quiz` | -- | `{ questions: [] }` | +| `POST` | `/api/topics/{id}/mastery-quiz/submit` | `{ answers: [] }` | `{ passed: boolean, score: number, mastery_update: number }` | + +**TopicContent shape:** +```json +{ + "topic_id": 5, + "topic_name": "Line Graphs", + "resources": [{ "id": 1, "name": "...", "resource_type": "pdf", "url": "..." }], + "ai_content": { + "explanation": "Line graphs show...", + "examples": ["Example 1...", "Example 2..."], + "key_points": ["Point 1", "Point 2"], + "model_used": "gpt-4o" + }, + "has_content_gap": true +} +``` + +**`has_content_gap`** = true when no human-uploaded resources exist for this topic, and AI content was generated to fill the gap. + +### 5.17 AI Coaching (NEW -- `coaching.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `POST` | `/api/coach/chat` | `{ message, context?: { page?, topic_id?, subject_id? }, history?: AiChatMessage[] }` | `{ message: string, suggestions?: string[] }` | +| `POST` | `/api/coach/hint` | `{ topic_id, question_id }` | `{ hint: string }` | +| `POST` | `/api/coach/explain` | `{ context, scores? }` | `{ explanation: string }` | +| `POST` | `/api/coach/suggest` | `{ subject_id? }` | `{ suggestions: string[], study_plan_tips: string[] }` | +| `POST` | `/api/coach/writing-help` | `{ text, task_type }` | `{ feedback, improved, grammar_notes: string[] }` | +| `GET` | `/api/coach/tip` | Query: `context` | `{ title, content, category }` | + +All coaching endpoints use GPT-4o with appropriate system prompts. + +### 5.18 AI Utilities (NEW -- `analytics.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `POST` | `/api/ai/search` | `{ query }` | `AiSearchResult[]` | +| `POST` | `/api/ai/insights` | `{ data: {} }` | `AiInsight[]` | +| `GET` | `/api/ai/alerts` | -- | `AiAlert[]` | +| `POST` | `/api/ai/report-narrative` | `{ report_type, data: {} }` | `{ narrative: string }` | +| `POST` | `/api/ai/batch-optimize` | `{ batch_id }` | `AiBatchOptimization[]` | +| `POST` | `/api/ai/grade-suggest` | `{ submission_id, text, rubric_id? }` | `AiGradingResult` | + +### 5.19 Analytics (NEW -- `analytics.service.ts`) + +| Method | Path | Params | Response | +|--------|------|--------|----------| +| `GET` | `/api/analytics/student` | Query params | Dashboard data | +| `GET` | `/api/analytics/class` | Query params | Class analytics | +| `GET` | `/api/analytics/subject` | Query params | Subject analytics | +| `GET` | `/api/analytics/content-gaps` | Query: `subject_id?` | `{ gaps: [{ topic_id, topic_name, resource_count }] }` | + +### 5.20 LMS Bridge (NEW -- `lms.service.ts`) + +| Method | Path | Request | Response | +|--------|------|---------|----------| +| `GET` | `/api/courses` | Query: `page`, `size`, `search`, `status` | `PaginatedResponse` | +| `GET` | `/api/courses/{id}` | -- | `Course` | +| `POST` | `/api/courses` | `CourseCreateRequest` | `Course` | +| `PATCH` | `/api/courses/{id}` | `Partial` | `Course` | +| `DELETE` | `/api/courses/{id}` | -- | `{ success: boolean }` | +| `POST` | `/api/courses/ai-generate` | `{ title, subject_id?, level? }` | `{ outline: {} }` | +| `GET` | `/api/batches` | Query: pagination | `PaginatedResponse` | +| `GET` | `/api/batches/{id}` | -- | `Batch` | +| `POST` | `/api/batches` | `Partial` | `Batch` | +| `PATCH` | `/api/batches/{id}` | `Partial` | `Batch` | +| `DELETE` | `/api/batches/{id}` | -- | `{ success: boolean }` | +| `GET` | `/api/timetable` | Query: `course_id?`, `teacher_id?`, `batch_id?` | `TimetableSession[]` | +| `POST` | `/api/timetable` | `Partial` | `TimetableSession` | +| `GET` | `/api/attendance` | Query: `course_id?`, `student_id?`, `date?` | `AttendanceRecord[]` | +| `POST` | `/api/attendance` | `{ course_id, date, records: [{student_id, status}] }` | `{ success: boolean }` | +| `GET` | `/api/grades` | Query: `course_id?`, `student_id?` | `GradeRecord[]` | + +**Course shape:** See `src/types/lms.ts` -- must include: `id`, `title`, `code`, `subject_id?`, `subject_name?`, `instructor_id`, `instructor_name`, `description`, `level`, `modules[]`, `enrolled`, `max_capacity`, `status`, `start_date`, `end_date`, `progress?`. + +--- + +## 6. Data Model Specification + +All data model details are in **Section 30 of `ENCOACH_UNIFIED_SRS.md`** (existing models: 30.1, new models: 30.2-30.4). Refer to that document for the complete field-level specification. + +**Key relationships:** +``` +Subject (1) → (N) Domain (1) → (N) Topic (1) → (N) LearningObjective +Topic (M) ↔ (M) Topic (prerequisites -- self-referencing M2M) +Topic (M) ↔ (M) Resource +Student (1) → (N) Proficiency (per topic) +Student (1) → (N) LearningPlan (per subject) → (N) LearningPlanItem (per topic) +Exam → Subject (optional M2O) +Exam → Topic[] (optional M2M) +``` + +--- + +## 7. AI Service Integration + +The following external AI services are already configured (API keys in `encoach_core/data/constants.xml` and server `.env`): + +| Service | Purpose | Config Key | +|---------|---------|-----------| +| OpenAI GPT-4o | Content generation, grading, coaching, plan generation | `encoach.openai_api_key` | +| OpenAI GPT-3.5-turbo | Lightweight tasks (tips, search, suggestions) | Same key | +| OpenAI Whisper (local) | Speech-to-text | Local model | +| AWS Polly (Neural) | Text-to-speech for listening exams | `encoach.aws_access_key_id`, `encoach.aws_secret_access_key` | +| ELAI | AI avatar video generation | `encoach.elai_api_key` | +| GPTZero | AI writing detection | `encoach.gptzero_api_key` | +| FAISS + SentenceTransformers | Semantic similarity for training tips | Local model (`all-MiniLM-L6-v2`) | + +**New AI tasks to implement:** + +| Task | Model | Endpoint | +|------|-------|----------| +| Diagnostic question generation | GPT-4o | `/api/diagnostic/start`, `/answer` | +| Learning plan generation | GPT-4o | `/api/learning-plan/generate` | +| Topic content generation | GPT-4o | `/api/topics/{id}/generate-content` | +| Practice question generation | GPT-4o | `/api/topics/{id}/practice` | +| Mastery quiz generation | GPT-4o | `/api/topics/{id}/mastery-quiz` | +| AI coaching chat | GPT-4o | `/api/coach/chat` | +| Study suggestions | GPT-3.5-turbo | `/api/coach/suggest` | +| Writing feedback | GPT-4o | `/api/coach/writing-help` | +| Grade explanation | GPT-4o | `/api/coach/explain` | +| Contextual tips | GPT-3.5-turbo | `/api/coach/tip` | +| Semantic search | FAISS | `/api/ai/search` | +| Report narrative | GPT-4o | `/api/ai/report-narrative` | +| Data insights | GPT-4o | `/api/ai/insights` | +| Topic suggestion | GPT-4o | `/api/domains/{id}/ai-suggest` | +| Course outline generation | GPT-4o | `/api/courses/ai-generate` | +| Batch optimization | GPT-3.5-turbo | `/api/ai/batch-optimize` | + +--- + +## 8. Response Format Standards + +### 8.1 Paginated Responses + +All list endpoints that support pagination must return: + +```json +{ + "items": [...], + "total": 150, + "page": 1, + "size": 20, + "pages": 8 +} +``` + +Query parameters: `page` (1-based), `size` (default 20), `search` (text filter), `sort` (field name), `order` (asc/desc). + +### 8.2 Error Responses + +```json +{ + "error": "Human-readable error message", + "code": "ERROR_CODE", + "details": {} +} +``` + +HTTP status codes: 400 (validation), 401 (unauthorized), 403 (forbidden), 404 (not found), 500 (server error). + +### 8.3 Success Responses + +For write operations that don't return an entity: + +```json +{ + "success": true, + "message": "Optional message" +} +``` + +### 8.4 JWT Token + +- Token returned on login as `{ token: "...", user: {...} }` +- Frontend sends `Authorization: Bearer ` on every request +- 401 response = token expired, frontend redirects to login + +--- + +## 9. Non-Functional Requirements + +| Requirement | Specification | +|-------------|--------------| +| API response time (CRUD) | < 2 seconds | +| Diagnostic question generation | < 3 seconds | +| AI grading (writing/speaking) | < 60 seconds | +| Learning plan generation | < 10 seconds | +| AI content generation | < 15 seconds | +| Concurrent students per subject | 200 | +| Topics per subject | 500 | +| Resources per subject | 1,000 | +| Pagination on all list endpoints | Required | +| Server-side search/filter | Required | +| Soft delete for resources | Required (preserve completion records) | +| Proficiency weighted averages | Required (never direct overwrite) | +| Math content | LaTeX/KaTeX formatting in question text | +| IT content | Code blocks with language hints in question text | + +--- + +## 10. Implementation Priority + +| Phase | Modules | Endpoints | Estimated Effort | +|-------|---------|-----------|-----------------| +| **P0** | `encoach_taxonomy`, modify `encoach_exam`/`encoach_stats`/`encoach_training` | Taxonomy CRUD (14), Subject/Topic fields on existing models | 1 week | +| **P1** | `encoach_adaptive`, `encoach_adaptive_api`, `encoach_adaptive_ai` | Diagnostic (3), Proficiency (3), Learning Plan (5), Content (6) | 2 weeks | +| **P1** | `encoach_resources` | Resource CRUD (7) | 3 days | +| **P1** | `encoach_lms_api` | LMS bridge (13) | 1 week | +| **P2** | `encoach_adaptive_ai` (coaching) | AI Coaching (6), AI Utilities (5) | 1 week | +| **P2** | Analytics controllers | Analytics (4) | 3 days | +| **P3** | `encoach_sis`, `encoach_branding` | SIS (4), Branding | 1 week | +| **P3** | Multi-subject prompts/grading | Modify generation & grading modules | 3 days | +| **TOTAL** | 8 new modules + 7 modified | ~65 new endpoints | ~7 weeks | + +--- + +## Appendix: File Handoff Checklist + +| File/Folder | Purpose | Location | +|-------------|---------|----------| +| This document | Backend SRS | `ODOO_BACKEND_SRS_v3.md` | +| Product SRS | Full platform specification | `ENCOACH_UNIFIED_SRS.md` | +| TypeScript types | Response shape contracts | `encoach_frontend_new/src/types/*.ts` | +| Service layer | Endpoint path contracts | `encoach_frontend_new/src/services/*.ts` | +| Existing modules | Already-built backend | `ielts-be-v0/` | +| Developer feedback | Review of existing work | `ielts-be-v0/DEVELOPER_FEEDBACK.md` | +| AI keys & config | Service credentials | `ielts-be-v0/encoach_core/data/constants.xml` | + +--- + +*This document supersedes `ODOO_MIGRATION_SRS_v2.md`. The Odoo developer should treat this as the definitive backend specification. All endpoint paths, response shapes, and data models are aligned with the production frontend at `encoach_frontend_new/`.* diff --git a/docs/ODOO_DEVELOPER_HANDOFF.md b/docs/ODOO_DEVELOPER_HANDOFF.md new file mode 100644 index 00000000..06515860 --- /dev/null +++ b/docs/ODOO_DEVELOPER_HANDOFF.md @@ -0,0 +1,100 @@ +# Odoo Developer Handoff — Implementation Complete + +**Status:** All deliverables implemented and deployed to staging. +**Date:** March 11, 2026 + +--- + +## 1. Current Repositories + +| Repository | URL | Description | +|-----------|-----|-------------| +| **Frontend v2** | `https://git.albousalh.com/devops/encoach_frontend_new_v2.git` | React 18 SPA (93 pages, 37 services, 29 types) | +| **Backend v2** | `https://git.albousalh.com/devops/encoach_backend_new_v2.git` | Odoo 19 backend (27 custom + 14 OpenEduCat = 41 modules) | + +**Note:** The older repositories (`encoach_frontend_new_v1`, `encoach_be_odoo19`, `ielts-be-v0`) are superseded. All development should continue in the v2 repositories above. + +--- + +## 2. Staging Environment + +| Service | URL | +|---------|-----| +| **Frontend** | `http://5.189.151.117:3000` | +| **Backend (Odoo 19)** | `http://5.189.151.117:8069` | + +Deployment is automated via Git post-receive hooks. Push to `main` triggers rebuild. + +--- + +## 3. Documentation + +| Document | Status | Description | +|----------|--------|-------------| +| `ENCOACH_UNIFIED_SRS.md` (v2.0) | **Current** | Unified frontend + backend SRS with implementation traceability | +| `ENCOACH_ODOO19_BACKEND_SRS.md` (v3.0) | **Current** | Backend-specific SRS with all API contracts and data models | +| `ENCOACH_PRODUCT_DESCRIPTION.md` | **Current** | Non-technical product description | +| `ENCOACH_SYSTEM_FEATURES_GUIDE.md` | **Current** | System features guide by sidebar section | +| `ODOO_BACKEND_SRS_v3.md` | **Superseded** | Replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` v3.0 | +| `ODOO_MIGRATION_SRS_v2.md` | **Superseded** | Replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` v3.0 | +| `ODOO_MIGRATION_SRS.md` | **Superseded** | Replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` v3.0 | +| `ODOO_MIGRATION_FRONTEND_SRS.md` | **Superseded** | Replaced by `ENCOACH_UNIFIED_SRS.md` v2.0 | + +--- + +## 4. What Was Implemented + +### Frontend (93 page components) + +- **28 root pages** -- login, register, FAQ, exam runner, etc. +- **33 admin pages** -- full LMS/platform administration +- **19 student pages** -- courses, adaptive learning, communication +- **14 teacher pages** -- course builder, AI workbench, grading +- **37 API service modules** calling ~280+ backend endpoints +- **14 AI components** wired to real backend AI services + +### Backend (41 Odoo modules, ~377 API routes) + +- **`encoach_api`** -- 16 controller files, ~120 routes (auth, users, entities, roles, exams, assignments, classrooms, stats, subscriptions, tickets, approvals, training, generation, AI analytics) +- **`encoach_lms_api`** -- 28 controller files, ~209 routes (courses, students, teachers, batches, timetable, attendance, grades, departments, academic, admissions, subject registration, institutional exams, course assignments, student leave, fees, lessons, gradebook, student progress, library, activities, facilities, notifications, FAQ, courseware, plagiarism, communication) +- **`encoach_adaptive_api`** -- 6 controller files, ~41 routes (taxonomy, diagnostic, proficiency, learning plans, content, coaching) +- **`encoach_resources`** -- 1 controller file, ~7 routes (resource CRUD) + +### Beyond Original SRS + +The developer also implemented: + +- Student Leave Management (types, requests, approve/reject) +- Fees Management (plans, student fees, terms) +- Lessons Management (timetable-linked) +- Gradebook and Grading Assignments +- Student Progress Tracking (aggregate) +- Library Management (media, movements, cards) +- Activity Management (configurable types) +- Facility and Asset Management +- Roles CRUD, User Role Assignment, Authority Matrix + +--- + +## 5. AI Service Credentials + +The developer needs API keys for: + +| Service | Purpose | +|---------|---------| +| **OpenAI** | GPT-4o and Whisper -- grading, coaching, content generation, speech-to-text | +| **AWS Polly** | Text-to-speech (neural voices) | +| **ELAI** | AI avatar video generation | +| **GPTZero** | AI writing detection | + +These are in `06-All-Credentials-Inventory.md` locally. Keys must be provided **separately** -- never committed to the git repo. On staging, they are in `/opt/encoach/backend-v2/.env`. + +--- + +## 6. Frontend Code Reference + +For any new backend development, the frontend contract is defined in: + +- **`src/services/*.ts`** (37 files) -- every URL path, request body shape, and response handling +- **`src/types/*.ts`** (29 files) -- all TypeScript interfaces the frontend expects from the API +- **`src/hooks/queries/*.ts`** (21 files) -- TanStack Query cache keys and query configurations diff --git a/docs/ODOO_MIGRATION_FRONTEND_SRS.md b/docs/ODOO_MIGRATION_FRONTEND_SRS.md new file mode 100644 index 00000000..b9f06f05 --- /dev/null +++ b/docs/ODOO_MIGRATION_FRONTEND_SRS.md @@ -0,0 +1,910 @@ +# EnCoach Frontend API Migration SRS + +> **SUPERSEDED** -- This document has been replaced by `ENCOACH_UNIFIED_SRS.md` (v2.0). The migration is complete. The old `ielts-ui` Next.js frontend has been replaced by `encoach_frontend_new_v2` (React 18 + Vite + TypeScript), deployed at `http://5.189.151.117:3000`. + +## ielts-ui Migration Guide: Next.js API Routes to Odoo 19 + +**Version:** 1.0 +**Date:** March 11, 2026 +**Status:** ~~Active~~ **SUPERSEDED** +**Audience:** Frontend developer(s) maintaining ielts-ui +**Companion document:** `ODOO_MIGRATION_SRS.md` (backend Odoo specification) + +--- + +## Table of Contents + +1. [Migration Overview](#1-migration-overview) +2. [Authentication Migration](#2-authentication-migration) +3. [API Base URL Change](#3-api-base-url-change) +4. [Endpoint-by-Endpoint Migration Map](#4-endpoint-by-endpoint-migration-map) +5. [SSR Migration Strategy](#5-ssr-migration-strategy) +6. [Files to Delete](#6-files-to-delete) +7. [Migration Checklist](#7-migration-checklist) + +--- + +## 1. Migration Overview + +### 1.1 What Is Changing + +The ielts-ui Next.js application currently serves two roles: + +1. **Browser UI** -- React pages, components, Zustand stores, hooks +2. **BFF (Backend-for-Frontend)** -- 108 API route files in `src/pages/api/` that query MongoDB directly, manage iron-session cookies, and proxy AI requests + +After migration, ielts-ui serves **only the Browser UI**. All backend logic moves to Odoo 19. The `src/pages/api/` directory and all server-side database utilities are eliminated entirely. + +### 1.2 Current Architecture (what the frontend sees) + +``` +Browser (React) + │ + ├── axios/SWR/fetch calls to /api/* (same origin) + │ + └── Next.js API Routes (src/pages/api/) + ├── iron-session cookie auth + ├── MongoDB queries (via src/utils/*.be.ts) + └── Proxy to AI backend (via BACKEND_URL) +``` + +### 1.3 Target Architecture (what the frontend sees) + +``` +Browser (React) + │ + ├── axios/SWR/fetch calls to ODOO_URL/api/* (cross-origin) + │ └── Authorization: Bearer + │ + └── Odoo 19 (separate service) + ├── JWT auth + ├── PostgreSQL (replaces MongoDB) + └── All AI/ML handled internally by Odoo +``` + +### 1.4 What Does NOT Change + +- All React page components, layouts, and UI rendering +- Zustand stores (`src/stores/exam/`, `src/stores/preferencesStore.ts`, etc.) +- All components in `src/components/` (they still call the same API shapes -- only the base URL and auth mechanism change) +- Styling (Tailwind, DaisyUI) +- TypeScript interfaces in `src/interfaces/` +- Client-side routing +- Strapi CMS and the landing page + +### 1.5 Guiding Principle + +Odoo's REST API is designed to return the **same JSON shapes** the frontend currently expects. If the frontend calls `GET /api/users/list` and expects `{ users: [...], total: N }`, Odoo's `GET /api/users/list` returns the same shape. The migration is primarily a **transport change** (base URL + auth header), not a data format change. + +--- + +## 2. Authentication Migration + +This is the most significant structural change. + +### 2.1 Current Auth Pattern + +| Aspect | Current Implementation | +|--------|----------------------| +| **Identity provider** | Firebase Auth (`signInWithEmailAndPassword`) | +| **Session** | iron-session (encrypted httpOnly cookie named `eCrop/ielts`) | +| **Client auth** | Automatic -- browser sends cookie on same-origin requests | +| **SSR auth** | `withIronSessionSsr` + `getServerSideProps` reads `req.session.user` | +| **User hook** | `useUser()` calls `GET /api/user` via SWR; cookie sent automatically | +| **Config** | `src/lib/session.ts` (`SECRET_COOKIE_PASSWORD`, `cookieName`) | + +### 2.2 Target Auth Pattern + +| Aspect | Target Implementation | +|--------|----------------------| +| **Identity provider** | Odoo 19 native auth (`res.users`) | +| **Session** | JWT token issued by Odoo | +| **Client auth** | `Authorization: Bearer ` header on every request | +| **SSR auth** | JWT stored in httpOnly cookie; `getServerSideProps` reads cookie and calls Odoo | +| **User hook** | `useUser()` calls `GET ODOO_URL/api/user` with JWT header | +| **Config** | `NEXT_PUBLIC_API_URL` env var | + +### 2.3 New Auth Infrastructure + +Create a new file `src/lib/api.ts`: + +```typescript +import axios from "axios"; + +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + withCredentials: true, +}); + +api.interceptors.request.use((config) => { + const token = getToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + clearToken(); + if (typeof window !== "undefined") { + window.location.href = "/login"; + } + } + return Promise.reject(error); + } +); + +export default api; +``` + +JWT storage helpers (also in `src/lib/api.ts` or a separate `src/lib/auth.ts`): + +```typescript +import Cookies from "js-cookie"; + +const TOKEN_KEY = "encoach_token"; + +export function setToken(token: string) { + Cookies.set(TOKEN_KEY, token, { secure: true, sameSite: "lax" }); +} + +export function getToken(): string | undefined { + return Cookies.get(TOKEN_KEY); +} + +export function clearToken() { + Cookies.remove(TOKEN_KEY); +} +``` + +**Note:** Using a cookie for JWT storage (rather than localStorage) is recommended because `getServerSideProps` can read cookies from the request headers, enabling SSR auth. The cookie is NOT httpOnly in this approach (so JS can read it for the axios interceptor). If httpOnly is required, use a thin proxy or Next.js middleware to inject the header. + +### 2.4 Files That Change for Auth + +| File | Current | Target | +|------|---------|--------| +| `src/pages/login.tsx` | `axios.post("/api/login", { email, password })` | `api.post("/api/login", { email, password })` -- store JWT from response | +| `src/pages/register.tsx` | `fetch` to `/api/register` (SSR) + code lookup | `api.post("/api/register", payload)` -- store JWT from response | +| `src/pages/official-exam.tsx` | `axios.post("/api/logout")` | `api.post("/api/logout")` + `clearToken()` | +| `src/components/Sidebar.tsx` | `axios.post("/api/logout")` | `api.post("/api/logout")` + `clearToken()` | +| `src/components/MobileMenu.tsx` | `axios.post("/api/logout")` | `api.post("/api/logout")` + `clearToken()` | +| `src/utils/email.ts` | `axios.post("/api/reset/sendVerification")` | `api.post("/api/reset/sendVerification")` | +| `src/hooks/useUser.tsx` | SWR fetcher uses `axios.get("/api/user")` | SWR fetcher uses `api.get("/api/user")` | +| `src/lib/session.ts` | iron-session config | **DELETE** -- replaced by `src/lib/api.ts` | + +### 2.5 Login Flow Change + +**Current:** +1. User submits email + password +2. `POST /api/login` (same origin) +3. API route calls Firebase Auth, looks up user in MongoDB, saves to iron-session +4. Response: user JSON + `Set-Cookie` header +5. `mutateUser(response.data)` updates SWR cache + +**Target:** +1. User submits email + password +2. `POST ODOO_URL/api/login` (cross-origin) +3. Odoo validates credentials, returns `{ user: {...}, token: "jwt..." }` +4. Frontend stores JWT via `setToken(response.data.token)` +5. `mutateUser(response.data.user)` updates SWR cache + +**Login page change (`src/pages/login.tsx`):** + +```typescript +// BEFORE +const response = await axios.post("/api/login", { email, password }); +mutateUser(response.data); + +// AFTER +import api, { setToken } from "@/lib/api"; +const response = await api.post("/api/login", { email, password }); +setToken(response.data.token); +mutateUser(response.data.user); +``` + +### 2.6 Logout Flow Change + +**Current:** `POST /api/logout` destroys iron-session. + +**Target:** `POST ODOO_URL/api/logout` (optional server-side invalidation) + `clearToken()` client-side. + +```typescript +// BEFORE +await axios.post("/api/logout"); +mutateUser(null); +router.push("/login"); + +// AFTER +import api, { clearToken } from "@/lib/api"; +await api.post("/api/logout").catch(() => {}); +clearToken(); +mutateUser(null); +router.push("/login"); +``` + +--- + +## 3. API Base URL Change + +### 3.1 Environment Variable + +Add to `.env.local`: + +``` +NEXT_PUBLIC_API_URL=https://api.encoach.com +``` + +Development: + +``` +NEXT_PUBLIC_API_URL=http://localhost:8069 +``` + +### 3.2 Shared Axios Instance + +Replace all direct `axios` imports with the shared `api` instance from `src/lib/api.ts` (defined in Section 2.3). + +**Search and replace pattern across the codebase:** + +```typescript +// BEFORE (scattered across ~40 files) +import axios from "axios"; +axios.get("/api/..."); +axios.post("/api/..."); +axios.patch("/api/..."); +axios.delete("/api/..."); + +// AFTER +import api from "@/lib/api"; +api.get("/api/..."); +api.post("/api/..."); +api.patch("/api/..."); +api.delete("/api/..."); +``` + +The `/api/...` path stays the same -- the `baseURL` on the axios instance prepends the Odoo host. + +### 3.3 SWR Fetcher Update + +Update `src/hooks/useUser.tsx`: + +```typescript +// BEFORE +const fetcher = (url: string) => axios.get(url).then((res) => res.data); + +// AFTER +import api from "@/lib/api"; +const fetcher = (url: string) => api.get(url).then((res) => res.data); +``` + +### 3.4 fetch() Calls + +A few files use `fetch()` instead of axios (primarily `src/components/ExamEditor/ImportExam/WordUploader.tsx`). These also need the base URL and auth header: + +```typescript +// BEFORE +const res = await fetch(`/api/exam/${module}/import/`, { method: "POST", body: formData }); + +// AFTER +import { getToken } from "@/lib/api"; +const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/exam/${module}/import/`, { + method: "POST", + body: formData, + headers: { Authorization: `Bearer ${getToken()}` }, +}); +``` + +**Note:** `fetch()` calls to blob URLs (e.g., `fetch(blobUrl)` for audio/video previews) do NOT change. Only calls to `/api/*` endpoints change. + +--- + +## 4. Endpoint-by-Endpoint Migration Map + +For each API call the frontend makes, this section specifies the current URL, new Odoo URL, HTTP method, and every file that makes the call. + +**Convention:** Where the Odoo URL is the same path as the current URL (just different host), only the base URL changes. These are marked "Path unchanged." Where the Odoo URL differs, the new path is shown. + +### 4.1 Auth + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 1 | POST | `/api/login` | Path unchanged | `src/pages/login.tsx` | +| 2 | POST | `/api/logout` | Path unchanged | `src/pages/official-exam.tsx`, `src/components/Sidebar.tsx`, `src/components/MobileMenu.tsx` | +| 3 | POST | `/api/reset` | Path unchanged | `src/pages/login.tsx` | +| 4 | POST | `/api/reset/sendVerification` | Path unchanged | `src/utils/email.ts` | + +**Response change for login:** Odoo returns `{ user: {...}, token: "jwt..." }` instead of just the user object. Frontend must extract and store the token. + +### 4.2 User + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 5 | GET | `/api/user` | Path unchanged | `src/hooks/useUser.tsx` (SWR) | +| 6 | DELETE | `/api/user` | Path unchanged | (called from useUser context) | +| 7 | POST | `/api/users/update` | `PATCH /api/users/update` | `src/pages/profile.tsx`, `src/components/UserCard.tsx` | +| 8 | GET | `/api/users/list` | Path unchanged | `src/utils/groups.ts` | +| 9 | GET | `/api/users/{id}` | Path unchanged | `src/utils/groups.ts` | +| 10 | POST | `/api/users/controller` | Path unchanged | `src/components/Imports/StudentClassroomTransfer.tsx` | +| 11 | GET | `/api/users/balance` | Path unchanged | `src/hooks/useUserBalance.tsx` (if used) | + +**Note on #7:** The current frontend uses `POST` for user updates. Odoo convention is `PATCH`. Either Odoo can accept both, or the frontend changes the method. + +### 4.3 Registration / Codes + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 12 | POST | `/api/register` | Path unchanged | `src/pages/register.tsx` (SSR currently; move to client-side) | +| 13 | GET | `/api/code/{code}` | Path unchanged | `src/pages/register.tsx` | + +**Note on #12:** Registration is currently handled in `getServerSideProps`. With Odoo, move registration to a client-side form submission. The register page should call `api.post("/api/register", payload)` and store the returned JWT. + +### 4.4 Exams + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 14 | GET | `/api/exam` | Path unchanged | `src/hooks/useExams.tsx` | +| 15 | GET | `/api/exam/{module}?{params}` | Path unchanged | `src/utils/exams.ts` (`getExam`) | +| 16 | GET | `/api/exam/{module}/{id}` | Path unchanged | `src/utils/exams.ts` (`getExamById`) | +| 17 | POST | `/api/exam/{module}` | Path unchanged | `src/components/ExamEditor/SettingsEditor/reading/index.tsx`, `writing/index.tsx`, `listening/index.tsx`, `speaking/index.tsx`, `level.tsx` | +| 18 | PATCH | `/api/exam/{module}/{id}` | Path unchanged | `src/pages/(admin)/Lists/ExamList.tsx`, `src/pages/approval-workflows/[id]/index.tsx` | +| 19 | DELETE | `/api/exam/{module}/{id}` | Path unchanged | `src/pages/(admin)/Lists/ExamList.tsx` | +| 20 | GET | `/api/exam/avatars` | Path unchanged | `src/pages/generation.tsx` | +| 21 | GET/POST | `/api/exam/generate/{module}/{sectionId}` | Path unchanged | `src/components/ExamEditor/SettingsEditor/Shared/Generate.ts` | +| 22 | POST | `/api/exam/media/speaking` | Path unchanged | `src/components/ExamEditor/SettingsEditor/Shared/generateVideos.ts` | +| 23 | POST | `/api/exam/media/listening` | Path unchanged | `src/components/ExamEditor/SettingsEditor/listening/components.tsx` | +| 24 | POST | `/api/exam/media/instructions` | Path unchanged | `src/components/ExamEditor/Standalone/ListeningInstructions/index.tsx` | +| 25 | POST | `/api/exam/{module}/import/` | Path unchanged | `src/components/ExamEditor/ImportExam/WordUploader.tsx` (uses `fetch`) | + +### 4.5 Evaluation / Grading + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 26 | POST | `/api/evaluate/writing` | Path unchanged | `src/utils/evaluation.ts` | +| 27 | POST | `/api/evaluate/speaking` | Path unchanged | `src/utils/evaluation.ts` | +| 28 | POST | `/api/evaluate/interactiveSpeaking` | Path unchanged | `src/utils/evaluation.ts` | +| 29 | GET | `/api/evaluate/status?...` | Path unchanged | `src/utils/evaluation.ts` (polling) | +| 30 | GET | `/api/evaluate/fetchSolutions?...` | Path unchanged | `src/utils/evaluation.ts` | +| 31 | POST | `/api/grading` | Path unchanged | `src/pages/api/grading/index.ts` (if called by frontend) | +| 32 | POST | `/api/grading/multiple` | Path unchanged | (if called by frontend) | + +### 4.6 Storage + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 33 | POST | `/api/storage` | Path unchanged | `src/components/ExamEditor/SettingsEditor/writing/index.tsx`, `speaking/index.tsx`, `listening/index.tsx`, `level.tsx` | +| 34 | POST | `/api/storage/delete` | `DELETE /api/storage/{id}` (or keep POST) | `src/utils/evaluation.ts` | + +**Note:** Odoo will use `ir.attachment` for file storage. The response must return a publicly accessible URL for the uploaded file, matching the current behavior. + +### 4.7 Speaking Audio + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 35 | POST | `/api/speaking` | Path unchanged | `src/components/Solutions/Speaking.tsx`, `src/components/Solutions/InteractiveSpeaking.tsx` | + +### 4.8 Sessions + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 36 | POST | `/api/sessions` | Path unchanged | `src/stores/exam/index.ts` (Zustand `saveSession`) | +| 37 | DELETE | `/api/sessions/{id}` | Path unchanged | `src/components/Medium/SessionCard.tsx` | + +### 4.9 Stats + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 38 | POST | `/api/stats` | Path unchanged | `src/stores/exam/index.ts` (Zustand `saveStats`) | +| 39 | GET | `/api/stats/{id}` | Path unchanged | `src/pages/training/[id]/index.tsx` | +| 40 | POST | `/api/statistical` | Path unchanged | `src/pages/statistical.tsx` | + +### 4.10 Training + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 41 | POST | `/api/training` | Path unchanged | `src/pages/training/index.tsx` | +| 42 | GET | `/api/training/{id}` | Path unchanged | `src/pages/training/[id]/index.tsx` | +| 43 | GET | `/api/training/walkthrough` | Path unchanged | `src/pages/training/[id]/index.tsx` | + +### 4.11 Groups + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 44 | GET | `/api/groups?participant={userId}` | Path unchanged | `src/utils/groups.ts` | +| 45 | POST | `/api/groups` | Path unchanged | `src/pages/classrooms/create.tsx` | +| 46 | PATCH | `/api/groups/{id}` | Path unchanged | `src/pages/classrooms/[id].tsx` | +| 47 | DELETE | `/api/groups/{id}` | Path unchanged | `src/pages/classrooms/[id].tsx` | + +### 4.12 Entities + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 48 | POST | `/api/entities` | Path unchanged | `src/pages/entities/create.tsx` | +| 49 | PATCH | `/api/entities/{id}` | Path unchanged | `src/pages/entities/[id]/index.tsx` | +| 50 | DELETE | `/api/entities/{id}` | Path unchanged | `src/pages/entities/[id]/index.tsx` | +| 51 | PATCH | `/api/entities/{id}/users` | Path unchanged | `src/pages/entities/[id]/index.tsx` | + +### 4.13 Roles + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 52 | POST | `/api/roles` | Path unchanged | `src/pages/entities/[id]/roles/index.tsx` | +| 53 | PATCH | `/api/roles/{id}` | Path unchanged | `src/pages/entities/[id]/roles/[role].tsx` | +| 54 | DELETE | `/api/roles/{id}` | Path unchanged | `src/pages/entities/[id]/roles/[role].tsx` | +| 55 | POST | `/api/roles/{id}/users` | Path unchanged | `src/pages/entities/[id]/index.tsx` | + +### 4.14 Permissions + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 56 | PATCH | `/api/permissions/{id}` | Path unchanged | `src/pages/permissions/[id].tsx` | + +### 4.15 Payments + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 57 | POST | `/api/payments` | Path unchanged | `src/pages/payment-record.tsx` | +| 58 | PATCH | `/api/payments/{id}` | Path unchanged | `src/pages/payment-record.tsx` | +| 59 | DELETE | `/api/payments/{id}` | Path unchanged | `src/pages/payment-record.tsx` | +| 60 | GET/POST/DELETE | `/api/payments/files/{type}/{paymentId}` | Path unchanged | `src/components/PaymentAssetManager.tsx` | +| 61 | POST | `/api/paymob` | Path unchanged | `src/components/PaymobPayment.tsx` | + +**Bug fix opportunity:** In `src/pages/payment-record.tsx`, the PATCH call uses `api/payments/${id}` without a leading `/`. When switching to the shared axios instance with a `baseURL`, this will work correctly. But verify during testing. + +### 4.16 Assignments + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 62 | POST | `/api/assignments` | Path unchanged | `src/pages/assignments/creator/index.tsx` | +| 63 | PATCH | `/api/assignments/{id}` | Path unchanged | `src/pages/assignments/creator/[id].tsx`, `src/pages/assignments/[id].tsx` | +| 64 | DELETE | `/api/assignments/{id}` | Path unchanged | `src/pages/assignments/creator/[id].tsx`, `src/pages/assignments/[id].tsx` | +| 65 | POST | `/api/assignments/{id}/start` | Path unchanged | `src/pages/assignments/creator/[id].tsx`, `src/pages/assignments/[id].tsx` | + +### 4.17 Invites + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 66 | GET | `/api/invites/{decision}/{id}` | `POST /api/invites/{decision}/{id}` | `src/components/Medium/InviteCard.tsx`, `src/components/Medium/InviteWithUserCard.tsx` | + +**Note:** Currently uses GET for accept/decline actions (side-effectful GET). Odoo should accept POST. Consider updating the frontend to use POST -- it's a more correct HTTP method for state-changing operations. + +### 4.18 Approval Workflows + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 67 | POST | `/api/approval-workflows/create` | Path unchanged | `src/pages/approval-workflows/create.tsx` | +| 68 | PUT | `/api/approval-workflows/{id}` | Path unchanged | `src/pages/approval-workflows/[id]/index.tsx` | +| 69 | PUT | `/api/approval-workflows/{id}/edit` | Path unchanged | `src/pages/approval-workflows/[id]/edit.tsx` | +| 70 | DELETE | `/api/approval-workflows/{id}` | Path unchanged | `src/pages/approval-workflows/index.tsx` | + +### 4.19 Transcription + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 71 | POST | `/api/transcribe` | Path unchanged | (called from evaluation utils if needed) | + +### 4.20 Batch Users + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 72 | POST | `/api/batch_users` | Path unchanged | (admin import page) | + +### 4.21 Make User (Admin) + +| # | Method | Current URL | Odoo URL | Files | +|---|--------|-------------|----------|-------| +| 73 | POST | `/api/make_user` | Path unchanged | (admin user creation) | + +--- + +## 5. SSR Migration Strategy + +### 5.1 Current SSR Auth Pattern + +~35 pages use this pattern: + +```typescript +import { withIronSessionSsr } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; + +export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { + const user = await requestUser(req, res); + if (!user || !user.isVerified) return redirect("/login"); + return { props: { user } }; +}, sessionOptions); +``` + +### 5.2 Recommended: JWT Cookie + Server-Side Validation (Option A) + +Store the JWT in a regular cookie (set by `js-cookie` on login). In `getServerSideProps`, read the cookie from the request and call Odoo to validate. + +**New helper `src/lib/ssr-auth.ts`:** + +```typescript +import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next"; + +const API_URL = process.env.NEXT_PUBLIC_API_URL; +const TOKEN_COOKIE = "encoach_token"; + +export function withAuth

>( + handler: (ctx: GetServerSidePropsContext, user: User) => Promise> +) { + return async (ctx: GetServerSidePropsContext): Promise> => { + const token = ctx.req.cookies[TOKEN_COOKIE]; + if (!token) { + return { redirect: { destination: "/login", permanent: false } }; + } + + try { + const res = await fetch(`${API_URL}/api/user`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error("Unauthorized"); + const user = await res.json(); + if (!user.isVerified) { + return { redirect: { destination: "/login", permanent: false } }; + } + return handler(ctx, user); + } catch { + return { redirect: { destination: "/login", permanent: false } }; + } + }; +} +``` + +**Page migration example:** + +```typescript +// BEFORE +export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { + const user = await requestUser(req, res); + if (!user) return redirect("/login"); + // ... fetch additional data using user ... + return { props: { user, someData } }; +}, sessionOptions); + +// AFTER +export const getServerSideProps = withAuth(async (ctx, user) => { + // ... fetch additional data using user ... + return { props: { user, someData } }; +}); +``` + +### 5.3 Alternative: Client-Only Auth (Option B) + +Remove all `getServerSideProps` auth checks. Every page uses `useUser({ redirectTo: "/login" })` to protect itself. + +**Pros:** Simpler; no SSR auth at all. +**Cons:** Brief flash of unauthenticated content; pages that fetch data server-side need to move data fetching to client-side. + +### 5.4 Pages Requiring SSR Migration + +Every page below currently uses `withIronSessionSsr` and needs to switch to `withAuth` (Option A) or remove SSR auth (Option B): + +| Page | SSR Data Fetching | +|------|-------------------| +| `src/pages/index.tsx` | User only (redirects to dashboard) | +| `src/pages/login.tsx` | Reverse guard (redirect if already logged in) | +| `src/pages/dashboard/index.tsx` | User only | +| `src/pages/dashboard/teacher.tsx` | User + groups + assignments + stats | +| `src/pages/dashboard/student.tsx` | User + assignments + stats | +| `src/pages/dashboard/corporate.tsx` | User + entity data | +| `src/pages/dashboard/mastercorporate.tsx` | User + multi-entity data | +| `src/pages/dashboard/admin.tsx` | User + platform stats | +| `src/pages/dashboard/developer.tsx` | User | +| `src/pages/users/index.tsx` | User + user list | +| `src/pages/users/performance.tsx` | User + performance data | +| `src/pages/training/index.tsx` | User | +| `src/pages/training/[id]/index.tsx` | User + training data | +| `src/pages/tickets.tsx` | User + tickets | +| `src/pages/stats.tsx` | User + stats | +| `src/pages/statistical.tsx` | User | +| `src/pages/settings.tsx` | User | +| `src/pages/record.tsx` | User | +| `src/pages/profile.tsx` | User | +| `src/pages/popout.tsx` | User (session-based) | +| `src/pages/permissions/index.tsx` | User + permissions | +| `src/pages/permissions/[id].tsx` | User + permission detail | +| `src/pages/payment.tsx` | User | +| `src/pages/payment-record.tsx` | User + payments | +| `src/pages/official-exam.tsx` | User | +| `src/pages/generation.tsx` | User | +| `src/pages/exercises.tsx` | User + exam data | +| `src/pages/exam.tsx` | User + exam + session data | +| `src/pages/entities/index.tsx` | User + entities | +| `src/pages/entities/create.tsx` | User | +| `src/pages/entities/[id]/index.tsx` | User + entity detail | +| `src/pages/entities/[id]/roles/index.tsx` | User + roles | +| `src/pages/entities/[id]/roles/[role].tsx` | User + role detail | +| `src/pages/classrooms/*.tsx` | User + groups | +| `src/pages/assignments/*.tsx` | User + assignments | +| `src/pages/approval-workflows/*.tsx` | User + workflows | + +**For pages that only need user auth (no server-side data):** Replace `getServerSideProps` with client-side `useUser({ redirectTo: "/login" })`. + +**For pages that fetch additional data server-side:** Use `withAuth` and make the data-fetching calls to Odoo using the JWT token from the cookie. + +--- + +## 6. Files to Delete + +After migration, the following files are no longer needed. They represent the BFF layer being replaced by Odoo. + +### 6.1 API Route Files (108 files) + +Delete the entire `src/pages/api/` directory: + +**Auth & User:** +- `src/pages/api/login.ts` +- `src/pages/api/logout.ts` +- `src/pages/api/register.ts` +- `src/pages/api/user.ts` +- `src/pages/api/make_user.ts` +- `src/pages/api/batch_users.ts` +- `src/pages/api/reset/index.ts` +- `src/pages/api/reset/confirm.ts` +- `src/pages/api/reset/sendVerification.ts` +- `src/pages/api/reset/verify.ts` +- `src/pages/api/users/update.ts` +- `src/pages/api/users/search.ts` +- `src/pages/api/users/list.ts` +- `src/pages/api/users/controller.ts` +- `src/pages/api/users/balance.ts` +- `src/pages/api/users/agents/index.ts` +- `src/pages/api/users/agents/[code].ts` +- `src/pages/api/users/[id].ts` + +**Exams:** +- `src/pages/api/exam/index.ts` +- `src/pages/api/exam/upload.ts` +- `src/pages/api/exam/avatars.ts` +- `src/pages/api/exam/generate/[...module].ts` +- `src/pages/api/exam/media/[...module].ts` +- `src/pages/api/exam/media/poll.ts` +- `src/pages/api/exam/media/instructions.ts` +- `src/pages/api/exam/[module]/index.ts` +- `src/pages/api/exam/[module]/import.ts` +- `src/pages/api/exam/[module]/[id].ts` + +**Evaluation:** +- `src/pages/api/evaluate/writing.ts` +- `src/pages/api/evaluate/speaking.ts` +- `src/pages/api/evaluate/interactiveSpeaking.ts` +- `src/pages/api/evaluate/status.ts` +- `src/pages/api/evaluate/fetchSolutions.ts` + +**Grading:** +- `src/pages/api/grading/index.ts` +- `src/pages/api/grading/multiple.ts` + +**Stats & Sessions:** +- `src/pages/api/stats/index.ts` +- `src/pages/api/stats/update.ts` +- `src/pages/api/stats/disabled.ts` +- `src/pages/api/stats/user/[user].ts` +- `src/pages/api/stats/session/[session].ts` +- `src/pages/api/stats/[id]/index.ts` +- `src/pages/api/stats/[id]/[export]/pdf.tsx` +- `src/pages/api/statistical.ts` +- `src/pages/api/sessions/index.ts` +- `src/pages/api/sessions/[id].ts` + +**Training:** +- `src/pages/api/training/index.ts` +- `src/pages/api/training/[id].ts` +- `src/pages/api/training/user/[user].ts` +- `src/pages/api/training/walkthrough/index.ts` + +**Entities & Groups:** +- `src/pages/api/entities/index.ts` +- `src/pages/api/entities/users.ts` +- `src/pages/api/entities/groups.ts` +- `src/pages/api/entities/[id]/index.ts` +- `src/pages/api/entities/[id]/users.ts` +- `src/pages/api/groups/index.ts` +- `src/pages/api/groups/controller.ts` +- `src/pages/api/groups/[id].ts` + +**Roles & Permissions:** +- `src/pages/api/roles/index.ts` +- `src/pages/api/roles/[id]/index.ts` +- `src/pages/api/roles/[id]/users.ts` +- `src/pages/api/permissions/index.ts` +- `src/pages/api/permissions/bootstrap.ts` +- `src/pages/api/permissions/[id].ts` + +**Invites & Codes:** +- `src/pages/api/invites/index.ts` +- `src/pages/api/invites/[id].ts` +- `src/pages/api/invites/accept/[id].ts` +- `src/pages/api/invites/decline/[id].ts` +- `src/pages/api/code/index.ts` +- `src/pages/api/code/entities.ts` +- `src/pages/api/code/[id].ts` + +**Payments:** +- `src/pages/api/stripe.ts` +- `src/pages/api/paypal/index.ts` +- `src/pages/api/paypal/approve.ts` +- `src/pages/api/paypal/raas.ts` +- `src/pages/api/paymob/index.ts` +- `src/pages/api/paymob/webhook.ts` +- `src/pages/api/payments/index.ts` +- `src/pages/api/payments/[id].ts` +- `src/pages/api/payments/assigned.ts` +- `src/pages/api/payments/paypal.ts` +- `src/pages/api/payments/files/[type]/[paymentId].ts` +- `src/pages/api/packages/index.ts` +- `src/pages/api/packages/[id].ts` +- `src/pages/api/discounts/index.ts` +- `src/pages/api/discounts/[id].ts` + +**Assignments:** +- `src/pages/api/assignments/index.ts` +- `src/pages/api/assignments/corporate/index.ts` +- `src/pages/api/assignments/corporate/[id].ts` +- `src/pages/api/assignments/[id]/index.ts` +- `src/pages/api/assignments/[id]/start.ts` +- `src/pages/api/assignments/[id]/release.ts` +- `src/pages/api/assignments/[id]/archive.tsx` +- `src/pages/api/assignments/[id]/unarchive.tsx` +- `src/pages/api/assignments/[id]/[export]/pdf.tsx` +- `src/pages/api/assignments/[id]/[export]/excel.ts` + +**Approval Workflows:** +- `src/pages/api/approval-workflows/index.ts` +- `src/pages/api/approval-workflows/create.ts` +- `src/pages/api/approval-workflows/[id]/index.ts` +- `src/pages/api/approval-workflows/[id]/edit.ts` + +**Other:** +- `src/pages/api/storage/index.ts` +- `src/pages/api/storage/insert.ts` +- `src/pages/api/storage/delete.ts` +- `src/pages/api/speaking.ts` +- `src/pages/api/transcribe/index.ts` +- `src/pages/api/tickets/index.ts` +- `src/pages/api/tickets/[id].ts` +- `src/pages/api/tickets/assignedToUser/index.ts` +- `src/pages/api/hello.ts` + +### 6.2 Backend Utility Files (14 files) + +These files contain MongoDB query logic used by API routes. Delete all: + +- `src/utils/approval.workflows.be.ts` +- `src/utils/assignments.be.ts` +- `src/utils/codes.be.ts` +- `src/utils/disabled.be.ts` +- `src/utils/entities.be.ts` +- `src/utils/exams.be.ts` +- `src/utils/grading.be.ts` +- `src/utils/groups.be.ts` +- `src/utils/invites.be.ts` +- `src/utils/permissions.be.ts` +- `src/utils/roles.be.ts` +- `src/utils/sessions.be.ts` +- `src/utils/stats.be.ts` +- `src/utils/users.be.ts` + +### 6.3 Library Files (3 files) + +- `src/lib/session.ts` -- iron-session config (replaced by `src/lib/api.ts`) +- `src/lib/mongodb.ts` -- MongoDB connection (no longer needed) +- `src/lib/createWorkflowsOnExamCreation.ts` -- server-side workflow logic (moved to Odoo) + +**Keep:** +- `src/lib/utils.ts` -- general client-side utilities +- `src/lib/formidable-serverless.d.ts` -- only needed if any remaining file upload logic stays; likely deletable + +### 6.4 Firebase Files + +If Odoo replaces Firebase Auth entirely, also remove: + +- `src/firebase/` directory (Firebase client config) +- `src/constants/platform.json` (Firebase service account -- server-side only) +- `src/constants/staging.json` (Firebase service account -- server-side only) + +### 6.5 Dependencies to Remove from package.json + +``` +iron-session +firebase +firebase-admin +mongodb +@beam-australia/react-env (if no longer needed for client env) +bcrypt +formidable +``` + +### 6.6 Dependencies to Add + +``` +js-cookie (JWT cookie storage) +@types/js-cookie (TypeScript types) +``` + +--- + +## 7. Migration Checklist + +A step-by-step checklist for the frontend developer. Complete in order. + +### Phase 1: Infrastructure Setup + +- [ ] **1.1** Add `NEXT_PUBLIC_API_URL` to `.env.local` (point to Odoo dev instance) +- [ ] **1.2** Create `src/lib/api.ts` with shared axios instance, base URL, and JWT interceptor (Section 2.3) +- [ ] **1.3** Create `src/lib/auth.ts` with `setToken()`, `getToken()`, `clearToken()` helpers +- [ ] **1.4** Add `js-cookie` dependency: `yarn add js-cookie @types/js-cookie` +- [ ] **1.5** Create `src/lib/ssr-auth.ts` with `withAuth()` helper for SSR (Section 5.2) + +### Phase 2: Auth Migration + +- [ ] **2.1** Update `src/pages/login.tsx`: use `api.post()`, store JWT, extract user from response +- [ ] **2.2** Update `src/pages/register.tsx`: move registration to client-side, use `api.post()` +- [ ] **2.3** Update logout calls in `Sidebar.tsx`, `MobileMenu.tsx`, `official-exam.tsx`: use `api.post()` + `clearToken()` +- [ ] **2.4** Update `src/hooks/useUser.tsx`: use `api` instance for SWR fetcher +- [ ] **2.5** Update `src/utils/email.ts`: use `api` instance +- [ ] **2.6** Verify login/logout/register work end-to-end with Odoo + +### Phase 3: SSR Migration + +- [ ] **3.1** For each page in the SSR page list (Section 5.4), replace `withIronSessionSsr` with `withAuth` or client-side `useUser({ redirectTo: "/login" })` +- [ ] **3.2** Update pages that fetch server-side data to call Odoo with the JWT token +- [ ] **3.3** Verify all ~35 protected pages redirect correctly when not authenticated +- [ ] **3.4** Verify the login page redirects correctly when already authenticated + +### Phase 4: API Call Migration (domain by domain) + +For each domain, replace `axios` with `api` from `@/lib/api`: + +- [ ] **4.1** User endpoints (`src/pages/profile.tsx`, `src/components/UserCard.tsx`, `src/utils/groups.ts`, `src/components/Imports/StudentClassroomTransfer.tsx`) +- [ ] **4.2** Exam endpoints (`src/utils/exams.ts`, `src/hooks/useExams.tsx`, `src/pages/generation.tsx`, all `ExamEditor` files, `ExamList.tsx`) +- [ ] **4.3** Evaluation endpoints (`src/utils/evaluation.ts`) +- [ ] **4.4** Storage endpoints (all `ExamEditor/SettingsEditor` files, `src/utils/evaluation.ts`) +- [ ] **4.5** Speaking audio (`src/components/Solutions/Speaking.tsx`, `InteractiveSpeaking.tsx`) +- [ ] **4.6** Sessions (`src/stores/exam/index.ts`, `src/components/Medium/SessionCard.tsx`) +- [ ] **4.7** Stats (`src/stores/exam/index.ts`, `src/pages/training/[id]/index.tsx`, `src/pages/statistical.tsx`) +- [ ] **4.8** Training (`src/pages/training/index.tsx`, `src/pages/training/[id]/index.tsx`) +- [ ] **4.9** Groups (`src/utils/groups.ts`, `src/pages/classrooms/create.tsx`, `src/pages/classrooms/[id].tsx`) +- [ ] **4.10** Entities (`src/pages/entities/create.tsx`, `src/pages/entities/[id]/index.tsx`) +- [ ] **4.11** Roles (`src/pages/entities/[id]/roles/index.tsx`, `[role].tsx`) +- [ ] **4.12** Permissions (`src/pages/permissions/[id].tsx`) +- [ ] **4.13** Payments (`src/pages/payment-record.tsx`, `src/components/PaymentAssetManager.tsx`, `src/components/PaymobPayment.tsx`) +- [ ] **4.14** Assignments (`src/pages/assignments/creator/index.tsx`, `[id].tsx`, `src/pages/assignments/[id].tsx`) +- [ ] **4.15** Invites (`src/components/Medium/InviteCard.tsx`, `InviteWithUserCard.tsx`) +- [ ] **4.16** Approval workflows (`src/pages/approval-workflows/*.tsx`) +- [ ] **4.17** Codes (`src/pages/register.tsx`) +- [ ] **4.18** Remaining: transcribe, batch_users, make_user, statistical +- [ ] **4.19** Update `WordUploader.tsx` `fetch()` call with base URL and auth header + +### Phase 5: Cleanup + +- [ ] **5.1** Delete all 108 files in `src/pages/api/` +- [ ] **5.2** Delete all 14 `src/utils/*.be.ts` files +- [ ] **5.3** Delete `src/lib/session.ts`, `src/lib/mongodb.ts`, `src/lib/createWorkflowsOnExamCreation.ts` +- [ ] **5.4** Delete `src/firebase/` directory (if Firebase Auth fully removed) +- [ ] **5.5** Delete `src/constants/platform.json`, `src/constants/staging.json` +- [ ] **5.6** Remove unused dependencies from `package.json`: `iron-session`, `firebase`, `firebase-admin`, `mongodb`, `bcrypt`, `formidable` +- [ ] **5.7** Run `yarn install` to update lockfile +- [ ] **5.8** Run TypeScript compiler (`yarn tsc --noEmit`) and fix any remaining type errors +- [ ] **5.9** Run linter (`yarn lint`) and fix issues + +### Phase 6: Testing + +- [ ] **6.1** Test login flow (email + password -> JWT stored -> user loaded) +- [ ] **6.2** Test registration flow (individual and corporate) +- [ ] **6.3** Test logout (token cleared, redirect to login) +- [ ] **6.4** Test SSR protection (unauthenticated access to protected page -> redirect) +- [ ] **6.5** Test exam taking flow (load exam, answer questions, submit, save stats) +- [ ] **6.6** Test writing/speaking grading (submit, poll, receive result) +- [ ] **6.7** Test entity/group/classroom management (CRUD operations) +- [ ] **6.8** Test assignment lifecycle (create, start, release, archive) +- [ ] **6.9** Test payment flows (Stripe, PayPal, Paymob) +- [ ] **6.10** Test file uploads (exam media, profile pictures) +- [ ] **6.11** Test training content generation +- [ ] **6.12** Test ticket creation (from platform and landing page) +- [ ] **6.13** Test admin user management (create, update, batch import) +- [ ] **6.14** Cross-browser test (Chrome, Firefox, Safari) diff --git a/docs/ODOO_MIGRATION_SRS.md b/docs/ODOO_MIGRATION_SRS.md new file mode 100644 index 00000000..a8dde994 --- /dev/null +++ b/docs/ODOO_MIGRATION_SRS.md @@ -0,0 +1,2337 @@ +# EnCoach Platform -- Odoo 19 Migration SRS + +> **SUPERSEDED** -- This document has been replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` (v3.0) and `ENCOACH_UNIFIED_SRS.md` (v2.0). All content below is historical. The migration is complete and the system is deployed at `http://5.189.151.117:8069`. + +## Software Requirements Specification + +**Version:** 1.0 +**Date:** March 11, 2026 +**Status:** ~~Draft~~ **SUPERSEDED** + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Odoo Module Plan](#2-odoo-module-plan) +3. [Data Models](#3-data-models-odoo-models) +4. [REST API Specification](#4-rest-api-specification) +5. [Authentication & Authorization](#5-authentication--authorization) +6. [ielts-be Integration Specification](#6-ielts-be-integration-specification) +7. [Payment Integration](#7-payment-integration) +8. [Business Rules & Workflows](#8-business-rules--workflows) +9. [Data Migration Plan](#9-data-migration-plan) +10. [Non-Functional Requirements](#10-non-functional-requirements) + +--- + +## 1. Executive Summary + +### 1.1 Platform Overview + +EnCoach is an IELTS preparation and English proficiency testing platform serving students, teachers, corporate clients, and administrators. The platform provides: + +- Full IELTS exam simulation (Reading, Listening, Writing, Speaking, Level) +- AI-powered grading of writing and speaking responses +- AI-powered exam content generation +- Multi-tenant entity (organization) management +- Classroom and assignment management +- Training and personalized recommendations +- Subscription-based access with multiple payment providers +- Support ticket system + +### 1.2 Migration Rationale + +Replace the current custom Node.js/MongoDB backend with Odoo 19 to leverage Odoo's built-in ERP capabilities for user management, subscriptions, payments, and helpdesk while retaining the specialized AI/ML microservice. + +### 1.3 Scope Boundaries + +**In Scope (Odoo replaces):** + +- All MongoDB direct access from ielts-ui API routes (22+ collections, 60+ endpoints) +- Firebase Auth for user authentication +- All business logic in `ielts-ui/src/pages/api/` and `ielts-ui/src/utils/*.be.ts` +- Payment processing (Stripe, PayPal, Paymob) +- File storage (currently Firebase Storage) + +**Out of Scope (unchanged):** + +- ielts-ui browser/frontend code (React components, Zustand stores, pages) +- ielts-be (FastAPI) AI/ML microservice (grading, generation, TTS, transcription, training) +- Strapi CMS (encoachcms) +- Landing page (encoach-landing-page) + +### 1.4 Current Architecture + +``` +ielts-ui (Next.js 14) + ├── Browser UI (React + Zustand + Tailwind) + ├── API Routes (/pages/api/) ──── MongoDB (22+ collections) + │ └─── Firebase Auth + └── Proxy routes ─────────────── ielts-be (FastAPI) + ├── OpenAI GPT-4o + ├── Whisper (local STT) + ├── AWS Polly (TTS) + ├── ELAI (AI Video) + └── GPTZero (AI Detection) +``` + +### 1.5 Target Architecture + +``` +ielts-ui (Next.js 14) + └── Browser UI (unchanged) + │ + ▼ +Odoo 19 (Python + PostgreSQL) + ├── REST API Controllers (JSON) + ├── Business Logic (custom modules) + ├── PostgreSQL (all data) + ├── Odoo Attachments (file storage) + ├── Payment Providers (Stripe, PayPal, Paymob) + └── HTTP Client ──────────────── ielts-be (FastAPI, unchanged) + ├── OpenAI GPT-4o + ├── Whisper (local STT) + ├── AWS Polly (TTS) + ├── ELAI (AI Video) + └── GPTZero (AI Detection) +``` + +The ielts-ui frontend will be updated to point its API calls at Odoo instead of its own `/api/` routes. The Next.js API routes layer is eliminated entirely. Odoo becomes the sole backend, proxying AI/ML requests to the unchanged ielts-be service. + +--- + +## 2. Odoo Module Plan + +### 2.1 Standard Odoo Modules to Leverage + +| Odoo Module | Usage | +|-------------|-------| +| `base` / `res.users` / `res.partner` | User accounts, extended with EnCoach-specific fields | +| `payment` | Payment provider framework for Stripe, PayPal, Paymob | +| `product` | Subscription packages as products | +| `helpdesk` (or custom) | Support ticket management | +| `mail` | Email notifications (password reset, verification, invites) | + +### 2.2 Custom Modules to Develop + +| Module | Depends On | Complexity | Description | +|--------|------------|------------|-------------| +| `encoach_core` | `base`, `mail` | Medium | User type extensions, entity management, roles, permissions, codes, invites | +| `encoach_exam` | `encoach_core` | High | Exam models for 5 modules (reading, listening, writing, speaking, level) with complex nested exercise structures | +| `encoach_classroom` | `encoach_core` | Low | Group/classroom management with participant tracking | +| `encoach_assignment` | `encoach_core`, `encoach_exam` | Medium | Assignment lifecycle (create, start, release, archive) linking exams to students | +| `encoach_stats` | `encoach_core`, `encoach_exam` | Medium | Exam sessions, per-exercise statistics, score tracking | +| `encoach_evaluation` | `encoach_core`, `encoach_exam` | Medium | Async grading records for writing/speaking, integration with ielts-be | +| `encoach_training` | `encoach_core` | Low | Training content storage, walkthrough state, proxy to ielts-be | +| `encoach_subscription` | `encoach_core`, `product`, `payment` | Medium | Packages, discounts, subscription expiry management, Stripe/PayPal/Paymob | +| `encoach_registration` | `encoach_core` | Medium | Multi-path registration (individual, corporate, admin-created), registration codes | +| `encoach_ticket` | `encoach_core` | Low | Support tickets (or extend `helpdesk`) | +| `encoach_api` | All above | High | REST JSON controllers exposing ~60 endpoints for frontend consumption | + +### 2.3 Module Dependency Graph + +``` +encoach_api + ├── encoach_core + │ ├── base / res.users / res.partner + │ └── mail + ├── encoach_exam + │ └── encoach_core + ├── encoach_classroom + │ └── encoach_core + ├── encoach_assignment + │ ├── encoach_core + │ └── encoach_exam + ├── encoach_stats + │ ├── encoach_core + │ └── encoach_exam + ├── encoach_evaluation + │ ├── encoach_core + │ └── encoach_exam + ├── encoach_training + │ └── encoach_core + ├── encoach_subscription + │ ├── encoach_core + │ ├── product + │ └── payment + ├── encoach_registration + │ └── encoach_core + └── encoach_ticket + └── encoach_core +``` + +--- + +## 3. Data Models (Odoo Models) + +This section specifies every Odoo model required, with field-level detail. Each model maps from one or more current MongoDB collections. + +### 3.1 `encoach.user` (extends `res.users`) + +**Source:** MongoDB `users` collection + +Extend `res.users` with the following fields. The standard `res.users` already provides `login` (email), `name`, `password`, and `active`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `encoach_type` | Selection | Yes | One of: `student`, `teacher`, `corporate`, `admin`, `developer`, `agent`, `mastercorporate` | +| `encoach_status` | Selection | Yes | One of: `active`, `disabled`, `payment_due`. Default: `active` | +| `profile_picture` | Binary / URL | No | Profile image. Default: `/defaultAvatar.png` | +| `bio` | Text | No | User biography | +| `focus` | Selection | No | `academic` or `general`. Default: `academic` | +| `is_first_login` | Boolean | No | Default: `True` | +| `is_verified` | Boolean | No | Email verification status. Default: `False` | +| `subscription_expiration_date` | Datetime | No | When the subscription expires | +| `registration_date` | Datetime | Yes | Account creation date | +| `last_login` | Datetime | No | Last login timestamp | +| `level_reading` | Float | No | Current reading level (0-9). Default: 0 | +| `level_listening` | Float | No | Current listening level (0-9). Default: 0 | +| `level_writing` | Float | No | Current writing level (0-9). Default: 0 | +| `level_speaking` | Float | No | Current speaking level (0-9). Default: 0 | +| `desired_level_reading` | Float | No | Target reading level. Default: 9 | +| `desired_level_listening` | Float | No | Target listening level. Default: 9 | +| `desired_level_writing` | Float | No | Target writing level. Default: 9 | +| `desired_level_speaking` | Float | No | Target speaking level. Default: 9 | +| `student_id` | Char | No | External student ID (student type only) | +| `average_level` | Float | No | Computed average across modules (student only) | +| `preferred_gender` | Selection | No | `male` or `female` (student only) | +| `preferred_topics` | Text (JSON) | No | JSON array of preferred topics (student only) | +| `phone` | Char | No | Phone number (demographic info) | +| `passport_id` | Char | No | Passport/national ID | +| `country_code` | Char | No | Country code | +| `company_name` | Char | No | Company name (corporate/agent) | +| `commercial_registration` | Char | No | Commercial registration number (agent) | +| `company_arab_name` | Char | No | Company name in Arabic (agent) | +| `entity_ids` | Many2many | No | Link to `encoach.entity` via `encoach.user.entity.rel` | +| `permission_ids` | Many2many | No | Link to `encoach.permission` | +| `legacy_id` | Char | No | Original MongoDB/Firebase UID for migration | + +### 3.2 `encoach.user.entity.rel` + +**Purpose:** Many-to-many relationship between users and entities with a role. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | The user | +| `entity_id` | Many2one(`encoach.entity`) | Yes | The entity | +| `role_id` | Many2one(`encoach.role`) | Yes | The user's role within this entity | + +### 3.3 `encoach.entity` + +**Source:** MongoDB `entities` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Entity display name (`label` in current system) | +| `licenses` | Integer | No | Number of licensed seats | +| `expiry_date` | Datetime | No | Entity license expiry | +| `payment_status` | Selection | No | `paid`, `unpaid`, `trial` | +| `role_ids` | One2many(`encoach.role`) | No | Roles defined for this entity | +| `user_rel_ids` | One2many(`encoach.user.entity.rel`) | No | Users in this entity | +| `group_ids` | One2many(`encoach.group`) | No | Groups belonging to this entity | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.4 `encoach.role` + +**Source:** MongoDB `roles` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Role display name (`label` in current system) | +| `entity_id` | Many2one(`encoach.entity`) | Yes | Parent entity | +| `is_default` | Boolean | No | Whether this is the default role for new entity members | +| `permissions` | Text (JSON) | No | JSON object of granular permission flags | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.5 `encoach.group` + +**Source:** MongoDB `groups` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Group/classroom name | +| `admin_id` | Many2one(`res.users`) | Yes | Group administrator | +| `entity_id` | Many2one(`encoach.entity`) | No | Owning entity | +| `participant_ids` | Many2many(`res.users`) | No | Group members | +| `disable_editing` | Boolean | No | Whether members can be modified. Default: `False` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.6 `encoach.exam` + +**Source:** MongoDB `reading`, `listening`, `writing`, `speaking`, `level` collections + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | No | Display label | +| `module` | Selection | Yes | `reading`, `listening`, `writing`, `speaking`, `level` | +| `min_timer` | Integer | No | Minimum time in seconds | +| `is_diagnostic` | Boolean | No | Diagnostic exam flag. Default: `False` | +| `variant` | Char | No | Exam variant identifier | +| `difficulty` | Selection | No | `A1`, `A2`, `B1`, `B2`, `C1`, `C2` | +| `access` | Selection | Yes | `public`, `private`, `confidential`. Default: `public` | +| `shuffle` | Boolean | No | Whether to shuffle questions. Default: `False` | +| `requires_approval` | Boolean | No | Needs admin approval before use. Default: `False` | +| `approved` | Boolean | No | Approval status. Default: `False` | +| `owner_ids` | Many2many(`res.users`) | No | Exam owners/creators | +| `entity_ids` | Many2many(`encoach.entity`) | No | Entities with access | +| `parts` | Text (JSON) | Yes | **Full exam structure as JSON** (see Section 3.6.1) | +| `legacy_id` | Char | No | Original MongoDB ID | + +#### 3.6.1 Exam `parts` JSON Structure + +Exams use a deeply nested JSON structure for their content. Modeling this as relational tables would be extremely complex (exercises contain sub-questions, solutions, options, etc.). Store as a JSON field and validate at the application level. + +**Reading exam `parts` example:** + +```json +[ + { + "text": { "title": "Passage Title", "content": "Full passage text..." }, + "exercises": [ + { + "id": "uuid", + "type": "multipleChoice", + "prompt": "Choose the correct answer", + "questions": [ + { + "id": "uuid", + "prompt": "What does the author suggest?", + "options": [ + { "id": "a", "text": "Option A" }, + { "id": "b", "text": "Option B" } + ], + "solution": "a" + } + ] + }, + { + "id": "uuid", + "type": "trueFalse", + "prompt": "Are these statements true, false, or not given?", + "questions": [ + { "id": "uuid", "prompt": "Statement X", "solution": "TRUE" } + ] + }, + { + "id": "uuid", + "type": "fillBlanks", + "prompt": "Complete the summary", + "text": "The study found that ___ were ___", + "solutions": [{ "id": "1", "solution": "researchers" }], + "words": [{ "letter": "A", "word": "researchers" }], + "allowRepetition": false + }, + { + "id": "uuid", + "type": "writeBlanks", + "prompt": "Complete with no more than 3 words", + "maxWords": 3, + "text": "Scientists discovered ___", + "solutions": [{ "id": "1", "solution": ["the answer", "an answer"] }] + }, + { + "id": "uuid", + "type": "matchSentences", + "prompt": "Match the headings", + "variant": "HEADING", + "options": [{ "id": "i", "sentence": "Heading A" }], + "sentences": [{ "id": "1", "sentence": "Paragraph 1 summary", "solution": "i" }] + } + ] + } +] +``` + +**Listening exam `parts` example:** + +```json +[ + { + "script": [ + { "name": "Speaker A", "gender": "male", "text": "Hello..." }, + { "name": "Speaker B", "gender": "female", "text": "Hi..." } + ], + "exercises": [ + { + "id": "uuid", + "type": "multipleChoice", + "prompt": "Choose the correct answer", + "questions": [ + { + "id": "uuid", + "prompt": "What is the main topic?", + "options": [{ "id": "a", "text": "Weather" }], + "solution": "a" + } + ] + }, + { + "id": "uuid", + "type": "writeBlanks", + "prompt": "Complete the form", + "maxWords": 2, + "variant": "FORM", + "questions": [ + { "id": "uuid", "prompt": "Name: ___", "solution": ["John Smith"] } + ] + } + ] + } +] +``` + +**Writing exam `parts` example:** + +```json +[ + { + "exercises": [ + { + "id": "uuid", + "type": "writing", + "task": 1, + "prompt": "Describe the chart below...", + "attachment": "url-to-image-or-null" + } + ] + }, + { + "exercises": [ + { + "id": "uuid", + "type": "writing", + "task": 2, + "prompt": "Some people believe that..." + } + ] + } +] +``` + +**Speaking exam `parts` example:** + +```json +[ + { + "exercises": [ + { + "id": "uuid", + "type": "interactiveSpeaking", + "task": 1, + "prompts": [ + "Do you enjoy reading?", + "What kind of books do you prefer?" + ] + } + ] + }, + { + "exercises": [ + { + "id": "uuid", + "type": "speaking", + "task": 2, + "text": "Describe a place you have visited..." + } + ] + } +] +``` + +**Level exam `parts` example:** + +```json +[ + { + "text": { "title": "Passage", "content": "Read the following..." }, + "exercises": [ + { + "id": "uuid", + "type": "multipleChoice", + "prompt": "Answer the questions", + "questions": [ + { + "id": "uuid", + "prompt": "What is correct?", + "variant": "text", + "options": [{ "id": "a", "text": "Option A" }], + "solution": "a" + } + ] + }, + { + "id": "uuid", + "type": "fillBlanks", + "variant": "mc", + "prompt": "Fill in the blanks", + "text": "She ___ to school every day", + "solutions": [{ "id": "1", "solution": "goes" }], + "words": [{ "letter": "A", "word": "goes" }] + } + ] + } +] +``` + +### 3.7 `encoach.assignment` + +**Source:** MongoDB `assignments` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Assignment name | +| `assigner_id` | Many2one(`res.users`) | Yes | Creator (teacher/admin) | +| `assignee_ids` | Many2many(`res.users`) | Yes | Assigned students | +| `teacher_ids` | Many2many(`res.users`) | No | Supervising teachers | +| `exam_ids` | Many2many(`encoach.exam`) | Yes | Exams included | +| `instructor_gender` | Selection | No | `male` or `female` | +| `start_date` | Datetime | No | When assignment becomes available | +| `end_date` | Datetime | No | Assignment deadline | +| `auto_start` | Boolean | No | Auto-start at start_date. Default: `False` | +| `started` | Boolean | No | Whether assignment has been started. Default: `False` | +| `released` | Boolean | No | Whether results are released. Default: `False` | +| `archived` | Boolean | No | Archived status. Default: `False` | +| `entity_id` | Many2one(`encoach.entity`) | No | Owning entity | +| `results` | Text (JSON) | No | JSON object of student results | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.8 `encoach.session` + +**Source:** MongoDB `sessions` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | Student | +| `exam_id` | Many2one(`encoach.exam`) | No | Exam being taken | +| `assignment_id` | Many2one(`encoach.assignment`) | No | Related assignment (if any) | +| `start_time` | Datetime | Yes | Session start | +| `end_time` | Datetime | No | Session end | +| `status` | Selection | Yes | `in_progress`, `completed`, `abandoned` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.9 `encoach.stat` + +**Source:** MongoDB `stats` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | Student | +| `exam_id` | Many2one(`encoach.exam`) | Yes | Exam attempted | +| `exercise_id` | Char | Yes | Exercise UUID within the exam | +| `session_id` | Many2one(`encoach.session`) | Yes | Parent session | +| `assignment_id` | Many2one(`encoach.assignment`) | No | Related assignment | +| `date` | Datetime | Yes | Attempt timestamp | +| `module` | Selection | Yes | `reading`, `listening`, `writing`, `speaking`, `level` | +| `solutions` | Text (JSON) | No | JSON of user's answers | +| `exercise_type` | Char | No | Exercise type string | +| `time_spent` | Integer | No | Seconds spent | +| `inactivity` | Integer | No | Seconds of inactivity | +| `score` | Float | No | Score achieved | +| `is_disabled` | Boolean | No | Whether stat is disabled. Default: `False` | +| `shuffle_maps` | Text (JSON) | No | JSON mapping of shuffled question order | +| `pdf` | Binary | No | Generated PDF report | +| `is_practice` | Boolean | No | Practice attempt flag. Default: `False` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.10 `encoach.evaluation` + +**Source:** MongoDB `evaluation` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | Student being evaluated | +| `session_id` | Many2one(`encoach.session`) | Yes | Parent session | +| `exercise_id` | Char | Yes | Exercise UUID | +| `eval_type` | Selection | Yes | `writing`, `speaking`, `speaking_interactive` | +| `task` | Integer | Yes | Task number (1, 2, or 3) | +| `status` | Selection | Yes | `pending`, `in_progress`, `completed`, `error`. Default: `pending` | +| `result` | Text (JSON) | No | Grading result JSON (see Section 3.10.1) | +| `error` | Text | No | Error message if grading failed | +| `legacy_id` | Char | No | Original MongoDB ID | + +#### 3.10.1 Evaluation `result` JSON Structure + +**Writing result:** + +```json +{ + "comment": "Overall assessment of the student's writing...", + "overall": 6.5, + "task_response": { + "Task Achievement": { "grade": 6.0, "comment": "..." }, + "Coherence and Cohesion": { "grade": 7.0, "comment": "..." }, + "Lexical Resource": { "grade": 6.5, "comment": "..." }, + "Grammatical Range and Accuracy": { "grade": 6.5, "comment": "..." } + }, + "perfect_answer": "A model answer text...", + "fixed_text": "The student's text with corrections...", + "ai_detection": { "probability": 0.12 } +} +``` + +**Speaking result:** + +```json +{ + "comment": "Overall assessment of the student's speaking...", + "overall": 7.0, + "task_response": { + "Fluency and Coherence": { "grade": 7.0, "comment": "..." }, + "Lexical Resource": { "grade": 7.0, "comment": "..." }, + "Grammatical Range and Accuracy": { "grade": 6.5, "comment": "..." }, + "Pronunciation": { "grade": 7.5, "comment": "..." } + }, + "transcript": "Transcribed student speech...", + "fixed_text": "Corrected transcript...", + "perfect_answer": "A model answer...", + "solutions": ["url-to-audio-file"] +} +``` + +### 3.11 `encoach.package` + +**Source:** MongoDB `packages` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | No | Display name | +| `currency` | Char | Yes | Currency code (e.g., `USD`, `SAR`) | +| `duration` | Integer | Yes | Subscription duration value | +| `duration_unit` | Selection | Yes | `days`, `months`, `years` | +| `price` | Float | Yes | Package price | +| `active` | Boolean | No | Whether package is available. Default: `True` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.12 `encoach.payment` + +**Source:** MongoDB `payments` collection (corporate/agent payments) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `corporate_id` | Many2one(`res.users`) | Yes | Corporate user | +| `entity_id` | Many2one(`encoach.entity`) | No | Related entity | +| `agent_id` | Many2one(`res.users`) | No | Sales agent | +| `agent_commission` | Float | No | Agent commission percentage | +| `agent_value` | Float | No | Agent commission amount | +| `currency` | Char | Yes | Currency code | +| `value` | Float | Yes | Payment amount | +| `is_paid` | Boolean | No | Payment status. Default: `False` | +| `date` | Datetime | Yes | Payment date | +| `corporate_transfer` | Char | No | Corporate bank transfer reference | +| `commission_transfer` | Char | No | Commission transfer reference | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.13 `encoach.subscription.payment` + +**Source:** MongoDB `paypalpayments` collection (individual Stripe/PayPal/Paymob payments) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | Paying user | +| `provider` | Selection | Yes | `stripe`, `paypal`, `paymob` | +| `order_id` | Char | Yes | Provider's order/session ID | +| `status` | Selection | Yes | `pending`, `completed`, `failed` | +| `value` | Float | Yes | Amount paid | +| `currency` | Char | Yes | Currency code | +| `subscription_duration` | Integer | No | Duration value | +| `subscription_duration_unit` | Selection | No | `days`, `months`, `years` | +| `subscription_expiration_date` | Datetime | No | New expiry date after payment | +| `created_at` | Datetime | Yes | Payment timestamp | +| `checkout_data` | Text (JSON) | No | Raw provider checkout data | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.14 `encoach.ticket` + +**Source:** MongoDB `tickets` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `subject` | Char | Yes | Ticket subject | +| `description` | Text | Yes | Ticket description | +| `status` | Selection | Yes | `open`, `in_progress`, `resolved`, `closed`. Default: `open` | +| `ticket_type` | Char | No | Ticket category | +| `reporter_id` | Many2one(`res.users`) | No | Reporting user (null for public tickets) | +| `reported_from` | Char | No | Source (`platform`, `landing_page`) | +| `assigned_to_id` | Many2one(`res.users`) | No | Assigned support agent | +| `exam_information` | Text (JSON) | No | Related exam context | +| `date` | Datetime | Yes | Creation date | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.15 `encoach.code` + +**Source:** MongoDB `codes` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `code` | Char | Yes | Registration code string (unique) | +| `creator_id` | Many2one(`res.users`) | No | User who created the code | +| `entity_id` | Many2one(`encoach.entity`) | No | Entity this code belongs to | +| `code_type` | Selection | No | Code type category | +| `expiry_date` | Datetime | No | When the code expires | +| `creation_date` | Datetime | Yes | When the code was created | +| `user_id` | Many2one(`res.users`) | No | User who redeemed the code | +| `email` | Char | No | Email associated with this code | +| `name` | Char | No | Name associated with this code | +| `passport_id` | Char | No | Passport ID associated | +| `checkout` | Text (JSON) | No | Stripe checkout session data | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.16 `encoach.invite` + +**Source:** MongoDB `invites` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `entity_id` | Many2one(`encoach.entity`) | Yes | Entity being invited to | +| `from_user_id` | Many2one(`res.users`) | Yes | Inviter | +| `to_user_id` | Many2one(`res.users`) | Yes | Invitee | +| `status` | Selection | Yes | `pending`, `accepted`, `declined`. Default: `pending` | +| `created_at` | Datetime | Yes | Invite creation date | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.17 `encoach.permission` + +**Source:** MongoDB `permissions` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Permission display name | +| `permission_type` | Char | Yes | Permission type key | +| `topic` | Char | No | Permission topic/category | +| `user_ids` | Many2many(`res.users`) | No | Users granted this permission | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.18 `encoach.discount` + +**Source:** MongoDB `discounts` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `code` | Char | Yes | Discount code string | +| `percentage` | Float | Yes | Discount percentage (0-100) | +| `domain` | Char | No | Domain restriction | +| `valid_until` | Datetime | No | Expiry date | +| `active` | Boolean | No | Default: `True` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.19 `encoach.training` + +**Source:** MongoDB `training` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | Student | +| `created_at` | Datetime | Yes | Training generation date | +| `exams` | Text (JSON) | No | JSON array of exam performance summaries | +| `tips` | Text (JSON) | No | JSON object with categorized tips | +| `weak_areas` | Text (JSON) | No | JSON array: `[{ "area": "...", "comment": "..." }]` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.20 `encoach.walkthrough` + +**Source:** MongoDB `walkthrough` collection + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | User | +| `state` | Text (JSON) | No | JSON object tracking walkthrough progress | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.21 `encoach.approval.workflow` + +**Source:** MongoDB `configured-workflows` and `active-workflows` collections + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Workflow name | +| `workflow_type` | Selection | Yes | `configured`, `active` | +| `config` | Text (JSON) | No | Workflow configuration | +| `status` | Selection | No | `pending`, `approved`, `rejected` | +| `entity_id` | Many2one(`encoach.entity`) | No | Related entity | +| `legacy_id` | Char | No | Original MongoDB ID | + +--- + +## 4. REST API Specification + +All endpoints must return JSON. The `encoach_api` module implements Odoo JSON controllers (inheriting `http.Controller`) with `type='json'` or `type='http'` routes. + +**Base URL:** `https://{odoo-host}/api/` + +**Authentication:** All endpoints require a valid session/JWT unless marked `[PUBLIC]`. + +**Response format convention:** Match the current frontend expectations. All responses are JSON objects. Lists are returned as arrays. Errors use HTTP status codes with `{ "error": "message" }`. + +### 4.1 Authentication Endpoints + +#### `POST /api/login` + +Login with email and password. Returns user object and sets session. + +**Request:** +```json +{ + "email": "user@example.com", + "password": "secret" +} +``` + +**Response (200):** +```json +{ + "id": 1, + "email": "user@example.com", + "name": "John Doe", + "type": "student", + "status": "active", + "profilePicture": "/defaultAvatar.png", + "isFirstLogin": false, + "isVerified": true, + "focus": "academic", + "levels": { "reading": 6.5, "listening": 7.0, "writing": 6.0, "speaking": 6.5 }, + "desiredLevels": { "reading": 9, "listening": 9, "writing": 9, "speaking": 9 }, + "subscriptionExpirationDate": "2026-12-31T00:00:00Z", + "registrationDate": "2025-01-15T00:00:00Z", + "entities": [{ "id": 5, "role": "student" }], + "permissions": [] +} +``` + +**Error (401):** `{ "error": "Invalid credentials" }` + +#### `POST /api/register` + +Register a new user. Supports two registration types. + +**Request (individual):** +```json +{ + "type": "individual", + "email": "user@example.com", + "password": "secret", + "name": "John Doe", + "code": "OPTIONAL_REG_CODE" +} +``` + +**Request (corporate):** +```json +{ + "type": "corporate", + "email": "corp@example.com", + "password": "secret", + "name": "Corp Admin", + "corporateInformation": { + "companyName": "Acme Inc" + } +} +``` + +**Response (201):** User object (same shape as login response) + +**Business logic:** See Section 8.1 for full registration flow. + +#### `POST /api/logout` + +Destroy current session. + +**Response (200):** `{ "ok": true }` + +#### `GET /api/user` + +Get current authenticated user. + +**Response (200):** User object (same shape as login response) + +#### `POST /api/reset/sendVerification` + +Send email verification link. + +**Request:** `{ "email": "user@example.com" }` + +**Response (200):** `{ "ok": true }` + +#### `POST /api/reset/confirm` + +Reset password via token. + +**Request:** `{ "token": "reset-token", "newPassword": "new-secret" }` + +**Response (200):** `{ "ok": true }` + +### 4.2 User Endpoints + +#### `GET /api/users/list` + +List users linked to the current user (by entity, group, or admin scope). + +**Query params:** `page`, `limit`, `type`, `entity`, `group`, `search` + +**Response (200):** +```json +{ + "users": [ { ...userObject } ], + "total": 150 +} +``` + +#### `GET /api/users/{id}` + +Get a single user by ID. + +**Response (200):** User object + +#### `GET /api/users/search` + +Full-text search users by name or email. + +**Query params:** `q` (search string), `limit` + +**Response (200):** `[ { ...userObject } ]` + +#### `PATCH /api/users/update` + +Update user fields. Admins can update other users. Users can update their own profile. + +**Request:** +```json +{ + "id": 1, + "name": "Updated Name", + "bio": "New bio", + "focus": "general", + "levels": { "reading": 7.0 }, + "status": "active", + "subscriptionExpirationDate": "2027-01-01T00:00:00Z" +} +``` + +**Response (200):** Updated user object + +#### `DELETE /api/user` + +Delete the current user's account. + +**Response (200):** `{ "ok": true }` + +#### `GET /api/users/balance` + +Get user's subscription balance/remaining days. + +**Response (200):** `{ "balance": 45, "expirationDate": "2026-05-01T00:00:00Z" }` + +#### `GET /api/users/controller` + +Admin user CRUD controller. + +**Query params:** `action` (`list`, `create`, `update`, `delete`), relevant fields + +**Response:** Depends on action + +#### `POST /api/users/controller` + +Admin user creation/modification. + +**Request:** User creation/update payload + +**Response:** User object or confirmation + +#### `POST /api/make_user` + +Admin/corporate create a user (with auto-generated password). + +**Request:** +```json +{ + "email": "student@example.com", + "name": "Student Name", + "type": "student", + "entity": 5, + "group": 10, + "expiryDate": "2027-01-01", + "passport_id": "AB123456" +} +``` + +**Response (201):** `{ "user": { ...userObject }, "code": { ...codeObject } }` + +#### `POST /api/batch_users` + +Bulk import users. Odoo forwards to ielts-be for Firebase-compatible processing, then stores the resulting users. + +**Request:** `{ "makerID": "admin-id", "users": [ { ...userDTO } ] }` + +**Response (200):** `{ "ok": true }` + +**Integration:** Calls ielts-be `POST /api/user/import` (see Section 6). + +#### `GET /api/users/agents` `[PUBLIC]` + +List sales agent users. Used by the landing page. + +**Response (200):** `[ { "id": 1, "name": "Agent", "companyName": "...", "country": "SA" } ]` + +#### `GET /api/users/agents/{code}` + +Get agent by country code. + +**Response (200):** Agent user object + +### 4.3 Entity Endpoints + +#### `GET /api/entities` + +List entities the current user has access to. + +**Response (200):** `[ { ...entityObject } ]` + +#### `POST /api/entities` + +Create a new entity. + +**Request:** +```json +{ + "label": "ABC School", + "licenses": 100, + "expiryDate": "2027-01-01" +} +``` + +**Response (201):** Entity object + +#### `GET /api/entities/{id}` + +Get entity by ID with roles and user counts. + +**Response (200):** Entity object with nested `roles` and `userCount` + +#### `PATCH /api/entities/{id}` + +Update entity fields. + +**Request:** Partial entity object + +**Response (200):** Updated entity + +#### `GET /api/entities/{id}/users` + +List users belonging to an entity. + +**Query params:** `page`, `limit`, `role` + +**Response (200):** `{ "users": [...], "total": 50 }` + +#### `GET /api/entities/{id}/groups` + +List groups in an entity. + +**Response (200):** `[ { ...groupObject } ]` + +### 4.4 Group Endpoints + +#### `GET /api/groups` + +List groups accessible to the current user. + +**Query params:** `entity` + +**Response (200):** `[ { ...groupObject, "participantCount": 25 } ]` + +#### `POST /api/groups` + +Create a group. + +**Request:** +```json +{ + "name": "Class A", + "entity": 5, + "participants": [1, 2, 3] +} +``` + +**Response (201):** Group object + +#### `GET /api/groups/{id}` + +Get group with participant list. + +**Response (200):** Group object with `participants` array + +#### `PATCH /api/groups/{id}` + +Update group (add/remove participants, rename). + +**Request:** Partial group object + +**Response (200):** Updated group + +### 4.5 Role Endpoints + +#### `GET /api/roles` + +List roles for an entity. + +**Query params:** `entity` + +**Response (200):** `[ { ...roleObject } ]` + +#### `POST /api/roles` + +Create a role. + +**Request:** `{ "label": "Teacher", "entityID": 5, "permissions": { ... }, "isDefault": false }` + +**Response (201):** Role object + +#### `GET /api/roles/{id}` + +Get role details. + +#### `PATCH /api/roles/{id}` + +Update role permissions. + +#### `GET /api/roles/{id}/users` + +List users with this role. + +### 4.6 Permission Endpoints + +#### `GET /api/permissions` + +List all permission definitions. + +**Response (200):** `[ { "id": 1, "type": "exam_create", "topic": "exams", "users": [1,2,3] } ]` + +#### `POST /api/permissions/bootstrap` + +Initialize default permissions for an entity. + +#### `PATCH /api/permissions/{id}` + +Update a permission (add/remove users). + +### 4.7 Invite Endpoints + +#### `GET /api/invites` + +List invites for the current user (sent and received). + +**Response (200):** `[ { ...inviteObject } ]` + +#### `POST /api/invites` + +Send an invite. + +**Request:** `{ "entity": 5, "to": "user@example.com" }` + +**Response (201):** Invite object + +#### `POST /api/invites/accept/{id}` + +Accept an invite. + +**Response (200):** `{ "ok": true }` + +**Business logic:** Add user to entity with the default role. + +#### `POST /api/invites/decline/{id}` + +Decline an invite. + +**Response (200):** `{ "ok": true }` + +### 4.8 Code Endpoints + +#### `GET /api/code` + +List registration codes. + +**Query params:** `entity`, `creator` + +**Response (200):** `[ { ...codeObject } ]` + +#### `POST /api/code` + +Create a registration code. + +**Request:** `{ "entity": 5, "expiryDate": "2027-01-01", "type": "student" }` + +**Response (201):** Code object + +#### `GET /api/code/{id}` + +Get code details. + +#### `DELETE /api/code/{id}` + +Delete a code. + +#### `GET /api/code/entities` + +List codes grouped by entity. + +### 4.9 Exam Endpoints + +#### `GET /api/exam/{module}` + +List exams for a module. + +**Path params:** `module` = `reading` | `listening` | `writing` | `speaking` | `level` + +**Query params:** `entity`, `access`, `difficulty`, `page`, `limit` + +**Response (200):** `[ { ...examObject (without full parts for listing) } ]` + +#### `GET /api/exam/{module}/{id}` + +Get a single exam with full `parts` JSON. + +**Response (200):** Full exam object including nested exercises + +#### `POST /api/exam/upload` + +Upload an exam document for parsing. Proxied to ielts-be. + +**Request:** Multipart form with file + +**Response (200):** Parsed exam object + +**Integration:** Forwards to ielts-be endpoint (see Section 6). + +#### `POST /api/exam/generate/{module}` + +Generate exam content using AI. Proxied to ielts-be. + +**Path params:** `module` = `reading` | `listening` | `writing` | `speaking` | `level` + +**Request:** Module-specific generation parameters (varies) + +**Response (200):** Generated content + +**Integration:** Forwards to ielts-be endpoint (see Section 6). + +#### `POST /api/exam/media/{module}` + +Generate media (audio/video) for an exam. Proxied to ielts-be. + +**Request:** Module-specific media parameters + +**Response (200):** Media file (audio/video bytes) or URL + +**Integration:** Forwards to ielts-be endpoint (see Section 6). + +#### `GET /api/exam/avatars` + +List available speaking avatars. Proxied to ielts-be. + +**Response (200):** `[ { "id": "avatar_1", "name": "...", "thumbnail": "..." } ]` + +#### `POST /api/exam/{module}/import` + +Import an exam from file. Proxied to ielts-be. + +**Request:** Multipart form with `exercises` and optional `solutions` files + +**Response (200):** Parsed exam object. Odoo should then persist the exam to `encoach.exam`. + +#### `POST /api/exam` (save) + +Save an exam (new or updated) to the database. + +**Request:** Full exam object with `parts` JSON + +**Response (201/200):** Saved exam object with ID + +### 4.10 Assignment Endpoints + +#### `GET /api/assignments` + +List assignments. + +**Query params:** `assigner`, `assignee`, `entity`, `archived`, `page`, `limit` + +**Response (200):** `[ { ...assignmentObject } ]` + +#### `POST /api/assignments` + +Create an assignment. + +**Request:** +```json +{ + "name": "Week 1 Practice", + "assignees": [1, 2, 3], + "exams": [10, 11], + "startDate": "2026-04-01", + "endDate": "2026-04-07", + "instructorGender": "female", + "entity": 5, + "teachers": [4], + "autoStart": true +} +``` + +**Response (201):** Assignment object + +#### `GET /api/assignments/{id}` + +Get assignment details with participant results. + +#### `PATCH /api/assignments/{id}` + +Update assignment. + +#### `POST /api/assignments/{id}/start` + +Start an assignment (make it available to students). + +**Response (200):** `{ "ok": true }` + +#### `POST /api/assignments/{id}/release` + +Release assignment results to students. + +**Response (200):** `{ "ok": true }` + +#### `POST /api/assignments/{id}/archive` + +Archive an assignment. + +#### `POST /api/assignments/{id}/unarchive` + +Unarchive an assignment. + +#### `GET /api/assignments/corporate` + +List assignments for corporate dashboard. + +### 4.11 Session Endpoints + +#### `GET /api/sessions` + +List sessions for a user. + +**Query params:** `user`, `exam`, `assignment` + +**Response (200):** `[ { ...sessionObject } ]` + +#### `POST /api/sessions` + +Create a new exam session. + +**Request:** `{ "exam": 10, "assignment": 5 }` + +**Response (201):** Session object with ID + +#### `GET /api/sessions/{id}` + +Get session details. + +#### `PATCH /api/sessions/{id}` + +Update session (e.g., mark as completed). + +### 4.12 Stats Endpoints + +#### `GET /api/stats` + +List stats. + +**Query params:** `user`, `exam`, `session`, `module`, `assignment`, `page`, `limit` + +**Response (200):** `[ { ...statObject } ]` + +#### `POST /api/stats` + +Create a stat record (exercise attempt result). + +**Request:** +```json +{ + "user": 1, + "exam": 10, + "exercise": "uuid-string", + "session": 20, + "module": "reading", + "solutions": { ... }, + "type": "multipleChoice", + "timeSpent": 120, + "inactivity": 5, + "score": 0.85, + "assignment": 5, + "isPractice": false +} +``` + +**Response (201):** Stat object + +#### `GET /api/stats/{id}` + +Get a single stat. + +#### `PATCH /api/stats/{id}` + +Update a stat (e.g., disable, set PDF). + +#### `GET /api/stats/user/{userId}` + +Get all stats for a user. + +#### `GET /api/stats/session/{sessionId}` + +Get all stats for a session. + +#### `PATCH /api/stats/disabled` + +Bulk disable stats. + +#### `GET /api/stats/{id}/{exportType}/pdf` + +Generate a PDF report for a stat. Calls ielts-be for grading summary. + +**Response:** PDF binary or `{ "url": "..." }` + +**Integration:** Calls ielts-be `POST /api/exam/grade/summary` for section-level evaluation. + +### 4.13 Grading / Evaluation Endpoints + +#### `POST /api/evaluate/writing` + +Submit a writing answer for AI grading. Creates an evaluation record and proxies to ielts-be. + +**Request:** +```json +{ + "userId": 1, + "sessionId": 20, + "exerciseId": "uuid", + "question": "Write about...", + "answer": "Student's essay text...", + "task": 1, + "attachment": "optional-image-url" +} +``` + +**Response (200):** `{ "ok": true }` (grading happens asynchronously) + +**Business logic:** +1. Create `encoach.evaluation` record with status `pending` +2. Forward request to ielts-be `POST /api/exam/grade/writing/{task}` +3. ielts-be processes asynchronously and updates the evaluation record + +#### `POST /api/evaluate/speaking` + +Submit speaking audio for AI grading. + +**Request:** Multipart form with `userId`, `sessionId`, `exerciseId`, `task`, and audio files (`audio_1`, `audio_2`, etc.) + +**Response (200):** `{ "ok": true }` (grading happens asynchronously) + +**Business logic:** Same pattern as writing grading. + +#### `POST /api/evaluate/interactiveSpeaking` + +Submit interactive speaking (multiple Q&A pairs). + +**Request:** Same as speaking but with `question_N` and `audio_N` pairs. + +**Response (200):** `{ "ok": true }` + +#### `GET /api/evaluate/{sessionId}/{exerciseId}` + +Poll for evaluation result. + +**Response (200):** +```json +{ + "status": "completed", + "result": { ...gradingResult } +} +``` + +#### `POST /api/grading/multiple` + +Grade multiple short-answer exercises at once. Proxied to ielts-be. + +**Request:** +```json +{ + "text": "passage text", + "questions": ["Q1", "Q2"], + "answers": ["A1", "A2"] +} +``` + +**Response (200):** `{ "exercises": [{ "id": "...", "correct": true, "correct_answer": "..." }] }` + +### 4.14 Training Endpoints + +#### `POST /api/training` + +Generate training content for a user. Proxied to ielts-be. + +**Request:** +```json +{ + "userID": 1, + "stats": [ { ...statSummary } ] +} +``` + +**Response (200):** `{ "id": "training-record-id" }` + +**Business logic:** Forward to ielts-be; store returned content in `encoach.training`. + +#### `POST /api/training/tips` + +Fetch personalized tips. Proxied to ielts-be. + +**Request:** `{ "context": "...", "question": "...", "answer": "...", "correct_answer": "..." }` + +**Response (200):** `{ "tips": "..." }` + +#### `GET /api/training/user/{userId}` + +Get stored training content for a user. + +**Response (200):** Training object with tips and weak areas + +#### `GET /api/training/walkthrough` + +Get walkthrough state. + +**Response (200):** `{ "state": { ... } }` + +#### `PATCH /api/training/walkthrough` + +Update walkthrough state. + +**Request:** `{ "state": { ... } }` + +### 4.15 Payment Endpoints + +#### `POST /api/stripe` + +Create a Stripe checkout session. + +**Request:** `{ "email": "user@example.com", "days": 30, "key": "package-key" }` + +**Response (200):** `{ "url": "https://checkout.stripe.com/..." }` + +**Webhook: `POST /api/stripe/webhook`** + +Handle Stripe webhook events (payment completed). See Section 7.1. + +#### `POST /api/paypal` + +Create a PayPal order. + +**Request:** `{ "amount": 99.00, "currency": "USD" }` + +**Response (200):** `{ "orderId": "PAYPAL_ORDER_ID" }` + +#### `POST /api/paypal/approve` + +Capture a PayPal payment after customer approval. + +**Request:** `{ "id": "PAYPAL_ORDER_ID", "duration": 30, "duration_unit": "days", "trackingId": "..." }` + +**Response (200):** `{ "ok": true }` + +**Business logic:** See Section 7.2. + +#### `POST /api/paymob` + +Create a Paymob payment intention. + +**Request:** `{ "amount": 500, "currency": "SAR" }` + +**Response (200):** `{ "clientSecret": "..." }` + +#### `POST /api/paymob/webhook` + +Handle Paymob transaction webhook. + +**Business logic:** See Section 7.3. + +#### `GET /api/payments` + +List corporate/agent payments. + +**Query params:** `corporate`, `entity`, `agent`, `isPaid` + +**Response (200):** `[ { ...paymentObject } ]` + +#### `POST /api/payments` + +Create a corporate payment record. + +**Request:** Payment object fields + +**Response (201):** Payment object + +#### `GET /api/payments/{id}` + +Get payment details. + +#### `PATCH /api/payments/{id}` + +Update payment (mark as paid, update transfers). + +**Business logic:** When `isPaid` is set to `true`, activate the corporate user's subscription (see Section 8.4). + +#### `DELETE /api/payments/{id}` + +Delete a payment record. + +#### `GET /api/payments/assigned` + +List payments assigned to an agent. + +### 4.16 Package Endpoints + +#### `GET /api/packages` `[PUBLIC]` + +List available subscription packages. Used by both the platform and landing page. + +**Response (200):** `[ { "id": 1, "currency": "USD", "duration": 30, "duration_unit": "days", "price": 29.99 } ]` + +#### `POST /api/packages` + +Create a package (admin only). + +**Request:** Package fields + +**Response (201):** Package object + +#### `GET /api/packages/{id}` + +Get package details. + +#### `PATCH /api/packages/{id}` + +Update a package. + +#### `DELETE /api/packages/{id}` + +Delete a package. + +### 4.17 Discount Endpoints + +#### `GET /api/discounts` + +List discount codes. + +**Response (200):** `[ { ...discountObject } ]` + +#### `POST /api/discounts` + +Create a discount code. + +**Request:** `{ "code": "SAVE20", "percentage": 20, "domain": "encoach.com", "validUntil": "2027-01-01" }` + +**Response (201):** Discount object + +#### `DELETE /api/discounts/{id}` + +Delete a discount code. + +### 4.18 Ticket Endpoints + +#### `GET /api/tickets` + +List tickets. + +**Query params:** `status`, `assignedTo`, `reporter`, `page`, `limit` + +**Response (200):** `[ { ...ticketObject } ]` + +#### `POST /api/tickets` `[PUBLIC]` + +Create a ticket. Used by both the platform and landing page contact form. + +**Request:** +```json +{ + "subject": "Cannot access exam", + "description": "When I click...", + "type": "bug", + "reportedFrom": "platform", + "examInformation": { "examId": 10, "module": "reading" } +} +``` + +**Response (201):** Ticket object + +#### `GET /api/tickets/{id}` + +Get ticket details. + +#### `PATCH /api/tickets/{id}` + +Update ticket (assign, change status). + +#### `GET /api/tickets/assignedToUser` + +List tickets assigned to the current support agent. + +### 4.19 Storage Endpoints + +#### `POST /api/storage/insert` + +Upload a file. Use Odoo's `ir.attachment` system. + +**Request:** Multipart form with file + +**Response (200):** `{ "url": "https://odoo-host/web/content/attachment_id" }` + +#### `GET /api/storage` + +List files for a context (e.g., user profile pictures, exam attachments). + +**Query params:** `context`, `user` + +**Response (200):** `[ { "name": "file.png", "url": "..." } ]` + +#### `DELETE /api/storage/{id}` + +Delete a file. + +### 4.20 Transcription Endpoint + +#### `POST /api/transcribe` + +Transcribe audio. Proxied to ielts-be. + +**Request:** Multipart form with audio file + +**Response (200):** Dialog/transcript object + +### 4.21 Approval Workflow Endpoints + +#### `GET /api/approval-workflows` + +List configured and active approval workflows. + +#### `POST /api/approval-workflows` + +Create or configure an approval workflow. + +#### `PATCH /api/approval-workflows/{id}` + +Approve or reject a workflow. + +### 4.22 Statistical Endpoint + +#### `GET /api/statistical` + +Get platform-wide statistics (admin dashboard). Proxied to ielts-be if needed. + +--- + +## 5. Authentication & Authorization + +### 5.1 Recommended Approach: Odoo Native Auth with JWT + +Use Odoo's built-in `res.users` authentication with a JWT token layer for the SPA frontend. + +**Flow:** + +1. Frontend sends `POST /api/login` with email + password +2. Odoo validates credentials against `res.users` +3. Odoo generates a JWT token (using a library like `PyJWT`) +4. Frontend stores the JWT and sends it as `Authorization: Bearer ` on every request +5. Odoo middleware validates the JWT on each request and loads the user context + +**JWT payload:** + +```json +{ + "uid": 1, + "email": "user@example.com", + "type": "student", + "exp": 1735689600 +} +``` + +**JWT secret:** Configured via Odoo system parameter `encoach.jwt_secret`. + +**Token expiry:** 24 hours (configurable). Frontend should handle token refresh. + +### 5.2 Alternative: Firebase Token Validation + +If Firebase Auth migration is too disruptive: + +1. Keep Firebase Auth for identity management +2. Frontend authenticates with Firebase, gets Firebase ID token +3. Odoo validates Firebase token using Google's public keys +4. Odoo maps Firebase UID to `res.users` via `legacy_id` field + +This approach requires maintaining Firebase Auth as a dependency. + +### 5.3 Role-Based Access Control + +Map the 7 user types to Odoo security groups: + +| User Type | Odoo Group | Access Level | +|-----------|------------|-------------| +| `student` | `encoach_group_student` | Read own data, take exams, view own stats | +| `teacher` | `encoach_group_teacher` | Manage classrooms, assignments, view student stats in their groups | +| `corporate` | `encoach_group_corporate` | Manage entity, create users, view entity-wide data | +| `admin` | `encoach_group_admin` | Full platform access | +| `developer` | `encoach_group_developer` | Full access + system tools | +| `agent` | `encoach_group_agent` | View assigned payments, agent dashboard | +| `mastercorporate` | `encoach_group_mastercorporate` | Multi-entity corporate management | + +**Record rules (`ir.rule`):** + +- Students see only their own stats, sessions, evaluations, training +- Teachers see stats/sessions for students in their groups +- Corporate users see data for their entity +- Admins see all data +- Agents see their assigned payments + +### 5.4 API-Level Authorization + +Each endpoint should validate the user's type/group before processing: + +- Endpoints that create entities, manage users globally, or access admin dashboards require `admin` or `developer` type +- Endpoints that manage classrooms and assignments require `teacher`, `corporate`, or `admin` type +- Endpoints that create payments require `corporate`, `agent`, or `admin` type +- Public endpoints (packages, tickets/create, agents) require no authentication + +--- + +## 6. ielts-be Integration Specification + +Odoo communicates with the ielts-be microservice over HTTP. The ielts-be service is unchanged and expects a Bearer JWT token for authentication. + +### 6.1 Configuration + +| Odoo System Parameter | Description | +|----------------------|-------------| +| `encoach.ielts_be_url` | Base URL of ielts-be service (e.g., `http://ielts-be:8000/api`) | +| `encoach.ielts_be_jwt` | JWT token for authenticating with ielts-be | + +### 6.2 HTTP Client + +Implement a service class `EncoachAIClient` that: + +- Uses `requests` or `httpx` for HTTP calls +- Sets `Authorization: Bearer {jwt}` header on all requests +- Handles timeouts (30s for sync, 120s for generation endpoints) +- Handles errors and retries +- Logs all requests/responses for debugging + +### 6.3 Endpoint Mapping + +#### Exam Generation + +| Odoo calls | ielts-be endpoint | Method | Request | Response | +|-----------|-------------------|--------|---------|----------| +| Generate reading passage | `GET /api/exam/reading/{passage}` | GET | Query: `topic`, `word_count` | `{ "title": "...", "text": "..." }` | +| Generate reading exercises | `POST /api/exam/reading/` | POST | `ReadingDTO` JSON | `{ "exercises": [...] }` | +| Generate listening dialog | `GET /api/exam/listening/{section}` | GET | Query: `difficulty`, `topic` | `{ "dialog": {...} }` | +| Generate listening MP3 | `POST /api/exam/listening/media` | POST | `Dialog` JSON | MP3 binary | +| Transcribe audio | `POST /api/exam/listening/transcribe` | POST | Multipart: `audio` file | Dialog object | +| Generate listening instructions | `POST /api/exam/listening/instructions` | POST | `{ "text": "..." }` | MP3 binary | +| Generate listening exercises | `POST /api/exam/listening/` | POST | `ListeningExercisesDTO` JSON | `{ "exercises": [...] }` | +| Get speaking task | `GET /api/exam/speaking/{task}` | GET | Query: `topic`, `difficulty` | Speaking content | +| Generate speaking video | `POST /api/exam/speaking/media` | POST | `{ "text": "...", "avatar": "..." }` | `{ "status": "...", "result": "..." }` | +| Poll speaking video | `GET /api/exam/speaking/media/{vid_id}` | GET | - | `{ "status": "...", "result": "url" }` | +| Get speaking avatars | `GET /api/exam/speaking/avatars` | GET | - | Avatars list | +| Get writing task | `GET /api/exam/writing/{task}` | GET | Query: `difficulty`, `topic` | Writing prompt | +| Get writing task (academic) | `POST /api/exam/writing/{task}/attachment` | POST | Multipart: `file`, `difficulty` | Writing prompt with attachment | +| Generate level exercises | `POST /api/exam/level/` | POST | `LevelExercisesDTO` JSON | Generated exercises | +| Get level exam | `GET /api/exam/level/` | GET | - | Level exam | +| Get UTAS level exam | `GET /api/exam/level/utas` | GET | - | UTAS level exam | +| Import reading exam | `POST /api/exam/reading/import` | POST | Multipart: `exercises`, `solutions` | Parsed exam | +| Import listening exam | `POST /api/exam/listening/import` | POST | Multipart: `exercises`, `solutions` | Parsed exam + dialog | +| Import level exam | `POST /api/exam/level/import/` | POST | Multipart: `exercises`, `solutions` | Parsed level exam | +| Custom level exam | `POST /api/exam/level/custom/` | POST | JSON body | Custom level exam | + +#### Grading + +| Odoo calls | ielts-be endpoint | Method | Request | Response | Async? | +|-----------|-------------------|--------|---------|----------|--------| +| Grade writing | `POST /api/exam/grade/writing/{task}` | POST | `WritingGradeTaskDTO` JSON | `200 OK` | Yes | +| Grade speaking | `POST /api/exam/grade/speaking/{task}` | POST | Multipart form | `200 OK` | Yes | +| Grade short answers | `POST /api/exam/grade/short_answers` | POST | JSON | `{ "exercises": [...] }` | No | +| Grading summary | `POST /api/exam/grade/summary` | POST | `{ "sections": [...] }` | `{ "sections": [...] }` | No | + +**Async grading flow:** + +1. Odoo creates `encoach.evaluation` record with status `pending` +2. Odoo sends grading request to ielts-be with `userId`, `sessionId`, `exerciseId` +3. ielts-be processes asynchronously and writes result directly to the shared database (current pattern) OR calls back to Odoo (new pattern) +4. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` until status is `completed` + +**Important decision:** Currently ielts-be writes directly to the same MongoDB. With Odoo on PostgreSQL, one of these approaches is needed: + +- **Option A (recommended):** ielts-be calls an Odoo webhook `POST /api/evaluate/callback` to update the evaluation record when grading is done. Minimal change to ielts-be -- just change where it writes the result. +- **Option B:** Odoo polls ielts-be for grading status. Requires ielts-be to expose a status endpoint. +- **Option C:** ielts-be writes to a shared message queue (Redis, RabbitMQ) and Odoo consumes it. + +#### Training + +| Odoo calls | ielts-be endpoint | Method | Request | Response | +|-----------|-------------------|--------|---------|----------| +| Get training content | `POST /api/training/` | POST | Training request JSON | `{ "id": "..." }` | +| Get tips | `POST /api/training/tips` | POST | `FetchTipsDTO` JSON | Tips text | + +#### User Import + +| Odoo calls | ielts-be endpoint | Method | Request | Response | +|-----------|-------------------|--------|---------|----------| +| Batch import users | `POST /api/user/import` | POST | `BatchUsersDTO` JSON | `{ "ok": true }` | + +**Note:** With the migration away from Firebase Auth, the batch user import flow will need to be redesigned. Instead of importing to Firebase, users should be created directly in Odoo's `res.users`. The ielts-be batch import endpoint may become unnecessary unless Firebase Auth is retained (Option B in Section 5). + +--- + +## 7. Payment Integration + +### 7.1 Stripe + +**Odoo modules:** `payment`, `payment_stripe` (standard), or custom controller. + +**Checkout flow:** + +1. Frontend calls `POST /api/stripe` with `{ email, days, key, checkout }` +2. Odoo creates a Stripe Checkout session via Stripe API +3. Returns checkout URL to frontend +4. Customer completes payment on Stripe +5. Stripe sends webhook to `POST /api/stripe/webhook` + +**Webhook handling (`POST /api/stripe/webhook`):** + +1. Verify Stripe signature +2. On `checkout.session.completed`: + - If user exists with that email: extend `subscription_expiration_date` by `days` + - If user does not exist: create/update `encoach.code` with the email and expiry info, send registration email +3. Create `encoach.subscription.payment` record + +### 7.2 PayPal + +**Create order (`POST /api/paypal`):** + +1. Call PayPal API to create order +2. Return order ID to frontend + +**Capture payment (`POST /api/paypal/approve`):** + +1. Call PayPal API to capture order +2. On success: + - Find user by tracking ID + - Calculate new expiry: `max(current_expiry, now) + duration` + - Update `subscription_expiration_date` and set `status = active` + - Create `encoach.subscription.payment` record + - For corporate users: propagate subscription to all entity participants + +### 7.3 Paymob + +**Create intention (`POST /api/paymob`):** + +1. Call Paymob API to create payment intention +2. Return client secret to frontend + +**Webhook (`POST /api/paymob/webhook`):** + +1. Verify Paymob transaction signature +2. On success: + - Find user from transaction metadata + - Extend subscription (same logic as PayPal) + - Create `encoach.subscription.payment` record + - For entity payments: update entity `expiry_date` and propagate to entity users + +### 7.4 Corporate Payment Flow + +Corporate payments are manually tracked (bank transfers, not online): + +1. Admin/agent creates `encoach.payment` record with corporate, entity, agent details +2. When payment is confirmed (`is_paid` set to `true`): + - Set corporate user's `status` to `active` + - Optionally extend entity expiry + +--- + +## 8. Business Rules & Workflows + +### 8.1 Registration Flow + +#### Individual Registration (with or without code) + +1. Validate email is not already registered +2. Create `res.users` with `encoach_type = 'student'`, `encoach_status = 'active'` +3. Set defaults: `is_first_login = True`, `is_verified = False`, `focus = 'academic'`, levels all 0, desired levels all 9 +4. If a registration `code` is provided: + a. Look up `encoach.code` by code string + b. Validate code is not expired and not already used + c. Set user's `subscription_expiration_date` from code's `expiry_date` + d. If code has an `entity_id`: add user to that entity with the default role + e. Mark code as used (`user_id` = new user) +5. Send verification email + +#### Corporate Registration + +1. Validate email is not already registered +2. Create `res.users` with `encoach_type = 'corporate'` +3. Create `encoach.entity` for the corporate +4. Create 3 default `encoach.group` records under the entity: + - "Teachers" (admin = corporate user) + - "Students" (admin = corporate user) + - "Corporate" (admin = corporate user) +5. Add the corporate user to the entity with an admin role + +### 8.2 Subscription Management + +**Status transitions:** + +``` + payment received + payment_due ────────────────────────> active + ▲ │ + │ │ subscription expired + │ ▼ + └──────────────────────────── disabled + (can be reactivated) +``` + +**Automated checks (cron job):** + +- Run daily: check all users where `subscription_expiration_date < now()` +- Set `encoach_status = 'disabled'` for expired users +- Optionally send expiry warning emails 7 days before expiry + +**Subscription extension logic:** + +```python +new_expiry = max(current_expiry or now(), now()) + timedelta(days=days) +``` + +### 8.3 Entity Licensing + +- Entities have a `licenses` count (max users) +- When adding a user to an entity, check: `current_user_count < entity.licenses` +- If limit reached, reject the addition with an error +- Entity `expiry_date` is separate from individual user subscriptions +- When entity expires, all entity users lose access (optional business rule) + +### 8.4 Assignment Lifecycle + +``` +Created ──> Started ──> In Progress ──> Released ──> Archived + │ + └──> Unarchived ──> Released +``` + +1. **Created:** Teacher creates assignment with exams and assignees +2. **Started:** `auto_start` triggers at `start_date`, or teacher manually calls `/start` +3. **In Progress:** Students can take the exams. Stats are recorded +4. **Released:** Teacher calls `/release`. Students can view their results +5. **Archived:** Teacher calls `/archive`. Assignment hidden from active lists + +### 8.5 Grading Polling + +1. Student submits writing/speaking answer +2. Frontend calls `POST /api/evaluate/writing` or `/speaking` +3. Odoo creates `encoach.evaluation` with `status = 'pending'` +4. Odoo forwards to ielts-be +5. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` every 3-5 seconds +6. When ielts-be finishes (via callback or direct DB update), evaluation status becomes `completed` +7. Frontend receives the grading result and displays scores + feedback + +### 8.6 Corporate Payment Activation + +1. Corporate user or agent creates payment record +2. Admin verifies bank transfer received +3. Admin updates payment: `is_paid = true` +4. System sets corporate user's `status = 'active'` +5. System extends `subscription_expiration_date` for the corporate and optionally all entity members + +### 8.7 User Change Propagation + +When a user's entity membership or subscription changes, propagate to related records: + +- When a user is added to an entity: auto-add to the entity's default group (if configured) +- When a corporate's subscription is extended: optionally extend all participants' subscriptions +- When an entity's expiry is extended: optionally extend all entity users' subscriptions + +--- + +## 9. Data Migration Plan + +### 9.1 Migration Strategy + +Perform a one-time data migration from MongoDB + Firebase Auth to Odoo 19 (PostgreSQL). + +### 9.2 Collection-to-Model Mapping + +| MongoDB Collection | Odoo Model | Notes | +|-------------------|------------|-------| +| `users` | `res.users` + `res.partner` | Map `id` (Firebase UID) to `legacy_id`. Create Odoo-native passwords. | +| `entities` | `encoach.entity` | Direct mapping | +| `roles` | `encoach.role` | Direct mapping | +| `groups` | `encoach.group` | Resolve `admin` and `participants` to Odoo user IDs via `legacy_id` | +| `reading` | `encoach.exam` (module=reading) | `parts` stored as JSON | +| `listening` | `encoach.exam` (module=listening) | `parts` stored as JSON | +| `writing` | `encoach.exam` (module=writing) | `parts` stored as JSON | +| `speaking` | `encoach.exam` (module=speaking) | `parts` stored as JSON | +| `level` | `encoach.exam` (module=level) | `parts` stored as JSON | +| `assignments` | `encoach.assignment` | Resolve user references | +| `sessions` | `encoach.session` | Resolve user and exam references | +| `stats` | `encoach.stat` | Resolve user, exam, session references | +| `evaluation` | `encoach.evaluation` | Resolve user, session references | +| `packages` | `encoach.package` | Direct mapping | +| `payments` | `encoach.payment` | Resolve user references | +| `paypalpayments` | `encoach.subscription.payment` | Resolve user references | +| `tickets` | `encoach.ticket` | Resolve user references | +| `codes` | `encoach.code` | Resolve user, entity references | +| `invites` | `encoach.invite` | Resolve user, entity references | +| `permissions` | `encoach.permission` | Resolve user references | +| `discounts` | `encoach.discount` | Direct mapping | +| `training` | `encoach.training` | Resolve user references | +| `walkthrough` | `encoach.walkthrough` | Resolve user references | +| `active-workflows` | `encoach.approval.workflow` | Direct mapping | +| `configured-workflows` | `encoach.approval.workflow` | Direct mapping | + +### 9.3 Migration Script Requirements + +1. **Export MongoDB data** to JSON files (one per collection) +2. **Resolve references:** Build a mapping of `MongoDB _id / legacy_id -> Odoo ID` as records are imported +3. **Import order:** Users first (to resolve references), then entities, roles, groups, exams, then dependent records +4. **Password migration:** + - If using Odoo native auth: generate temporary passwords and force password reset on first login + - If keeping Firebase: no password migration needed (users authenticate via Firebase) +5. **File migration:** Migrate files from Firebase Storage to Odoo `ir.attachment` or a configured file storage +6. **Validation:** After import, run integrity checks: + - All Many2one references resolve to existing records + - User counts match between MongoDB and PostgreSQL + - Exam `parts` JSON is valid and parseable + - Subscription dates are preserved correctly + +### 9.4 Suggested Import Order + +1. `res.users` / `res.partner` (from `users`) +2. `encoach.entity` (from `entities`) +3. `encoach.role` (from `roles`) +4. `encoach.user.entity.rel` (from `users.entities` array) +5. `encoach.group` (from `groups`) +6. `encoach.exam` (from `reading`, `listening`, `writing`, `speaking`, `level`) +7. `encoach.package` (from `packages`) +8. `encoach.discount` (from `discounts`) +9. `encoach.code` (from `codes`) +10. `encoach.permission` (from `permissions`) +11. `encoach.assignment` (from `assignments`) +12. `encoach.session` (from `sessions`) +13. `encoach.stat` (from `stats`) +14. `encoach.evaluation` (from `evaluation`) +15. `encoach.payment` (from `payments`) +16. `encoach.subscription.payment` (from `paypalpayments`) +17. `encoach.ticket` (from `tickets`) +18. `encoach.invite` (from `invites`) +19. `encoach.training` (from `training`) +20. `encoach.walkthrough` (from `walkthrough`) +21. `encoach.approval.workflow` (from workflows) + +--- + +## 10. Non-Functional Requirements + +### 10.1 API Response Format + +All API responses must match the JSON structure the frontend currently expects. The frontend uses Axios and SWR to consume these APIs. + +- Success responses: HTTP 200/201 with JSON body +- Error responses: HTTP 4xx/5xx with `{ "error": "Human-readable message" }` +- List responses should support pagination: `{ "items": [...], "total": N }` or flat arrays depending on the current endpoint behavior +- Empty results: return `[]` for lists, `null` or `{}` for single objects + +### 10.2 CORS Configuration + +The following origins need CORS access: + +| Origin | Endpoints | +|--------|-----------| +| `https://platform.encoach.com` | All authenticated endpoints | +| `https://encoach.com` | `GET /api/packages`, `POST /api/tickets`, `GET /api/users/agents` | +| `http://localhost:3000` | All (development) | + +CORS headers must include: +- `Access-Control-Allow-Origin` (per origin, not `*`) +- `Access-Control-Allow-Credentials: true` +- `Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS` +- `Access-Control-Allow-Headers: Content-Type, Authorization` + +### 10.3 File Storage + +Replace Firebase Storage with Odoo's `ir.attachment` system or configure an external storage backend (S3, GCS). + +**Files stored:** +- User profile pictures +- Exam audio files (listening MP3s) +- Exam image attachments (writing Task 1 charts) +- Speaking video files +- PDF reports + +**Requirements:** +- Files must be accessible via URL (for the frontend to load) +- Support for audio (MP3), image (PNG/JPG), video (MP4), and PDF formats +- Maximum file size: 50 MB (audio/video), 10 MB (images), 5 MB (PDFs) + +### 10.4 Performance Requirements + +| Operation | Target Response Time | +|-----------|---------------------| +| Login / Auth | < 500ms | +| User list (paginated) | < 1s | +| Exam load (full JSON) | < 2s | +| Stat creation | < 500ms | +| Evaluation poll | < 300ms | +| Package list (public) | < 500ms | +| Ticket creation | < 500ms | +| File upload | < 5s (for files < 10 MB) | + +**Grading latency** is handled by ielts-be and is inherently slow (10-30s for writing, 30-60s for speaking). The polling pattern handles this. + +### 10.5 Scalability + +- Support at least 10,000 concurrent users +- Support 500,000+ stat records per exam module +- Efficient pagination for all list endpoints +- JSON fields (exam `parts`, solutions) indexed appropriately in PostgreSQL + +### 10.6 Security + +- All API communication over HTTPS in production +- JWT tokens with short expiry (24h) and secure signing +- Rate limiting on login endpoint (max 10 attempts per minute per IP) +- Input validation on all endpoints (prevent SQL injection, XSS) +- Stripe/PayPal/Paymob webhook signature verification +- No sensitive data in JWT payload (no passwords, no payment details) + +### 10.7 Logging & Monitoring + +- Log all API requests with timestamp, user, endpoint, response status +- Log all ielts-be integration calls with request/response +- Log all payment events +- Error alerting for failed grading callbacks, payment failures + +### 10.8 Testing + +- Unit tests for all business logic (registration, subscription, payments) +- Integration tests for all API endpoints +- End-to-end tests for critical flows (login -> take exam -> get graded) +- Load tests for concurrent exam-taking scenarios + +--- + +## Appendix A: Exercise Type Reference + +Complete list of exercise types used across all exam modules. + +### Reading Exercise Types + +| Type | Fields | Description | +|------|--------|-------------| +| `multipleChoice` | `questions[]` with `prompt`, `options[]`, `solution` | Select correct answer | +| `trueFalse` | `questions[]` with `prompt`, `solution` (TRUE/FALSE/NOT_GIVEN) | True/False/Not Given | +| `fillBlanks` | `text`, `solutions[]`, `words[]`, `allowRepetition` | Drag words into blanks | +| `writeBlanks` | `text`, `maxWords`, `solutions[]` | Type answers into blanks | +| `matchSentences` (HEADING) | `options[]`, `sentences[]` with `solution` | Match headings to paragraphs | +| `matchSentences` (IDEAMATCH) | `options[]`, `sentences[]` with `solution` | Match ideas to paragraphs | + +### Listening Exercise Types + +| Type | Fields | Description | +|------|--------|-------------| +| `multipleChoice` | `questions[]` | Standard MC | +| `multipleChoice3Options` | `questions[]` | MC with 3 options | +| `writeBlanks` (FILL) | `text`, `maxWords`, `solutions[]` | Fill in blanks from audio | +| `writeBlanks` (FORM) | `questions[]`, `maxWords` | Complete a form from audio | +| `writeBlanks` (QUESTIONS) | `questions[]`, `maxWords` | Answer questions from audio | +| `trueFalse` | `questions[]` | True/False from audio | + +### Writing Exercise Types + +| Type | Fields | Description | +|------|--------|-------------| +| `writing` (task 1) | `prompt`, optional `attachment` | Letter or chart description | +| `writing` (task 2) | `prompt` | Essay | + +### Speaking Exercise Types + +| Type | Fields | Description | +|------|--------|-------------| +| `interactiveSpeaking` (part 1) | `prompts[]` | Short Q&A | +| `speaking` (part 2) | `text` | Long turn monologue | +| `interactiveSpeaking` (part 3) | `prompts[]` | Discussion | + +### Level Exercise Types + +| Type | Fields | Description | +|------|--------|-------------| +| `multipleChoice` | `questions[]` with `variant` | MC with text/image variants | +| `fillBlanks` (mc) | `text`, `solutions[]`, `words[]` | Fill blanks with MC options | + +--- + +## Appendix B: Grading Rubric Details + +### Writing Rubric (IELTS Band Descriptors) + +| Criterion | Scale | Description | +|-----------|-------|-------------| +| Task Achievement / Task Response | 0-9 | How well the task requirements are addressed | +| Coherence and Cohesion | 0-9 | Logical organization and paragraph linking | +| Lexical Resource | 0-9 | Vocabulary range and accuracy | +| Grammatical Range and Accuracy | 0-9 | Grammar variety and correctness | +| **Overall** | 0-9 | Average of the four criteria | + +### Speaking Rubric (IELTS Band Descriptors) + +| Criterion | Scale | Description | +|-----------|-------|-------------| +| Fluency and Coherence | 0-9 | Flow, pace, logical connection | +| Lexical Resource | 0-9 | Vocabulary range and appropriateness | +| Grammatical Range and Accuracy | 0-9 | Grammar variety and correctness | +| Pronunciation | 0-9 | Clarity, stress, intonation | +| **Overall** | 0-9 | Average of the four criteria | + +--- + +## Appendix C: Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `ENCOACH_JWT_SECRET` | JWT signing secret | Random 256-bit string | +| `ENCOACH_IELTS_BE_URL` | ielts-be base URL | `http://ielts-be:8000/api` | +| `ENCOACH_IELTS_BE_JWT` | JWT for ielts-be auth | Pre-shared token | +| `STRIPE_SECRET_KEY` | Stripe secret key | `sk_live_...` | +| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | `whsec_...` | +| `PAYPAL_CLIENT_ID` | PayPal client ID | `AX...` | +| `PAYPAL_CLIENT_SECRET` | PayPal client secret | `EL...` | +| `PAYPAL_ACCESS_TOKEN_URL` | PayPal OAuth URL | `https://api.paypal.com/v1/oauth2/token` | +| `PAYMOB_API_KEY` | Paymob API key | `ZXlK...` | +| `PAYMOB_SECRET` | Paymob webhook secret | `...` | + +--- + +## Appendix D: Glossary + +| Term | Definition | +|------|-----------| +| **Entity** | An organization (school, company) that manages users under a shared license | +| **Group** | A classroom or user grouping within an entity | +| **Module** | An IELTS exam skill: Reading, Listening, Writing, Speaking, or Level | +| **Session** | A single exam-taking attempt by a student | +| **Stat** | The result of a single exercise attempt within a session | +| **Evaluation** | An AI-graded writing or speaking assessment | +| **Code** | A registration/invite code that grants access or subscription time | +| **Package** | A purchasable subscription plan | +| **ielts-be** | The AI/ML microservice that handles content generation and grading | +| **BFF** | Backend-for-Frontend; the current ielts-ui API routes layer being replaced | diff --git a/docs/ODOO_MIGRATION_SRS_v2.md b/docs/ODOO_MIGRATION_SRS_v2.md new file mode 100644 index 00000000..92f5e9a7 --- /dev/null +++ b/docs/ODOO_MIGRATION_SRS_v2.md @@ -0,0 +1,1294 @@ +# EnCoach Platform -- Odoo 19 Full Migration SRS (v2) + +> **SUPERSEDED** -- This document has been replaced by `ENCOACH_ODOO19_BACKEND_SRS.md` (v3.0) and `ENCOACH_UNIFIED_SRS.md` (v2.0). All content below is historical. The migration is complete and the system is deployed at `http://5.189.151.117:8069`. + +## Software Requirements Specification + +**Version:** 2.0 +**Date:** March 11, 2026 +**Status:** ~~Draft~~ **SUPERSEDED** +**Supersedes:** ODOO_MIGRATION_SRS.md (v1) +**Key change from v1:** The ielts-be (FastAPI) microservice is fully eliminated. All AI/ML functionality is absorbed into custom Odoo modules. + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Odoo Module Plan](#2-odoo-module-plan) +3. [Data Models](#3-data-models-odoo-models) +4. [REST API Specification](#4-rest-api-specification) +5. [Authentication & Authorization](#5-authentication--authorization) +6. [AI/ML Services Integration](#6-aiml-services-integration) +7. [Payment Integration](#7-payment-integration) +8. [Business Rules & Workflows](#8-business-rules--workflows) +9. [Data Migration Plan](#9-data-migration-plan) +10. [Non-Functional Requirements](#10-non-functional-requirements) + +--- + +## 1. Executive Summary + +### 1.1 Platform Overview + +EnCoach is an IELTS preparation and English proficiency testing platform serving students, teachers, corporate clients, and administrators. The platform provides: + +- Full IELTS exam simulation (Reading, Listening, Writing, Speaking, Level) +- AI-powered grading of writing and speaking responses (OpenAI GPT-4o) +- AI-powered exam content generation (OpenAI GPT-4o) +- AI-generated audio for listening exams (AWS Polly) +- AI-generated video for speaking exams (ELAI) +- Speech-to-text transcription (OpenAI Whisper) +- AI plagiarism/detection for writing (GPTZero) +- Personalized training recommendations (FAISS + sentence-transformers + GPT) +- Multi-tenant entity (organization) management +- Classroom and assignment management +- Subscription-based access with multiple payment providers +- Support ticket system + +### 1.2 Migration Scope + +**Everything moves to Odoo 19.** Both backend systems are replaced: + +1. **ielts-ui API routes** (Node.js, MongoDB, iron-session, Firebase Auth) -- 108 API route files, 22 MongoDB collections +2. **ielts-be** (Python, FastAPI, OpenAI, Whisper, AWS Polly, ELAI, GPTZero, FAISS) -- all AI/ML workloads + +**What stays unchanged:** + +- ielts-ui browser/frontend code (React components, Zustand stores, pages) +- Strapi CMS (encoachcms) +- Landing page (encoach-landing-page) + +### 1.3 Target Architecture + +``` +ielts-ui (Next.js 14) + └── Browser UI (unchanged) + │ + ▼ +Odoo 19 (Python + PostgreSQL) + ├── REST API Controllers (JSON) + ├── Business Logic (custom modules) + ├── AI/ML Modules + │ ├── OpenAI GPT-4o (grading + content generation) + │ ├── OpenAI Whisper (speech-to-text, local model) + │ ├── AWS Polly (text-to-speech for listening) + │ ├── ELAI (AI video for speaking) + │ ├── GPTZero (AI detection for writing) + │ └── FAISS + sentence-transformers (training search) + ├── PostgreSQL (all data) + ├── Odoo Attachments (file storage) + └── Payment Providers (Stripe, PayPal, Paymob) +``` + +There is no separate microservice. Odoo is the sole backend. + +--- + +## 2. Odoo Module Plan + +### 2.1 Standard Odoo Modules to Leverage + +| Odoo Module | Usage | +|-------------|-------| +| `base` / `res.users` / `res.partner` | User accounts, extended with EnCoach-specific fields | +| `payment` | Payment provider framework for Stripe, PayPal, Paymob | +| `product` | Subscription packages as products | +| `mail` | Email notifications (password reset, verification, invites) | +| `queue_job` (OCA) or `ir.cron` | Background task processing for async AI grading | + +### 2.2 Custom Modules to Develop + +| Module | Depends On | Complexity | Description | +|--------|------------|------------|-------------| +| `encoach_core` | `base`, `mail` | Medium | User type extensions, entity management, roles, permissions, codes, invites | +| `encoach_exam` | `encoach_core` | High | Exam models for 5 modules with complex nested exercise structures | +| `encoach_classroom` | `encoach_core` | Low | Group/classroom management with participant tracking | +| `encoach_assignment` | `encoach_core`, `encoach_exam` | Medium | Assignment lifecycle (create, start, release, archive) | +| `encoach_stats` | `encoach_core`, `encoach_exam` | Medium | Exam sessions, per-exercise statistics, score tracking | +| `encoach_evaluation` | `encoach_core`, `encoach_exam`, `encoach_ai` | Medium | Async grading records for writing/speaking | +| `encoach_training` | `encoach_core`, `encoach_ai` | Medium | Training content, walkthrough, FAISS semantic search | +| `encoach_subscription` | `encoach_core`, `product`, `payment` | Medium | Packages, discounts, subscription management, Stripe/PayPal/Paymob | +| `encoach_registration` | `encoach_core` | Medium | Multi-path registration (individual, corporate), codes, invites | +| `encoach_ticket` | `encoach_core` | Low | Support tickets | +| `encoach_ai` | `base` | **Very High** | Core AI services: OpenAI client, Whisper, AWS Polly, ELAI, GPTZero, FAISS | +| `encoach_ai_grading` | `encoach_ai`, `encoach_exam` | High | Writing/speaking grading logic, rubrics, prompt templates | +| `encoach_ai_generation` | `encoach_ai`, `encoach_exam` | High | Exam content generation for all 5 modules | +| `encoach_ai_media` | `encoach_ai` | High | Audio (Polly TTS), video (ELAI), transcription (Whisper) | +| `encoach_api` | All above | High | All REST JSON controllers for frontend consumption (~60 endpoints) | + +### 2.3 Module Dependency Graph + +``` +encoach_api + ├── encoach_core + │ ├── base / res.users / res.partner + │ └── mail + ├── encoach_exam + │ └── encoach_core + ├── encoach_classroom + │ └── encoach_core + ├── encoach_assignment + │ ├── encoach_core + │ └── encoach_exam + ├── encoach_stats + │ ├── encoach_core + │ └── encoach_exam + ├── encoach_evaluation + │ ├── encoach_core + │ ├── encoach_exam + │ └── encoach_ai + ├── encoach_training + │ ├── encoach_core + │ └── encoach_ai + ├── encoach_subscription + │ ├── encoach_core + │ ├── product + │ └── payment + ├── encoach_registration + │ └── encoach_core + ├── encoach_ticket + │ └── encoach_core + ├── encoach_ai (core AI services) + │ └── base + ├── encoach_ai_grading + │ ├── encoach_ai + │ └── encoach_exam + ├── encoach_ai_generation + │ ├── encoach_ai + │ └── encoach_exam + └── encoach_ai_media + └── encoach_ai +``` + +--- + +## 3. Data Models (Odoo Models) + +All models from v1 remain unchanged. This section adds the AI/ML-related models that were previously managed by ielts-be. + +> **Note:** For the full specification of the following models, refer to Section 3 of the v1 document (`ODOO_MIGRATION_SRS.md`). They are identical: +> `encoach.user`, `encoach.user.entity.rel`, `encoach.entity`, `encoach.role`, `encoach.group`, `encoach.exam`, `encoach.assignment`, `encoach.session`, `encoach.stat`, `encoach.evaluation`, `encoach.package`, `encoach.payment`, `encoach.subscription.payment`, `encoach.ticket`, `encoach.code`, `encoach.invite`, `encoach.permission`, `encoach.discount`, `encoach.walkthrough`, `encoach.approval.workflow` + +The following models are new or significantly expanded in v2: + +### 3.1 `encoach.training` (expanded) + +**Source:** MongoDB `training` collection (previously managed by ielts-be) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `user_id` | Many2one(`res.users`) | Yes | Student | +| `created_at` | Datetime | Yes | Training generation date | +| `exams` | Text (JSON) | No | JSON array of exam performance summaries: `[{ "id", "date", "performance_comment", "detailed_summary" }]` | +| `tips` | Text (JSON) | No | JSON object with categorized tips from FAISS search | +| `weak_areas` | Text (JSON) | No | JSON array: `[{ "area": "...", "comment": "..." }]` | +| `legacy_id` | Char | No | Original MongoDB ID | + +### 3.2 `encoach.training.tip` (new) + +**Source:** ielts-be `pathways_2_rw_with_ids.json` tips data + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `tip_id` | Char | Yes | Original tip identifier | +| `category` | Selection | Yes | `ct_focus`, `language_for_writing`, `reading_skill`, `strategy`, `writing_skill`, `word_link`, `word_partners` | +| `content` | Text | Yes | Tip text content | +| `embedding` | Binary | No | Pre-computed embedding vector (float32 array) | + +### 3.3 `encoach.ai.job` (new) + +Tracks async AI jobs (grading, video generation). + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `job_type` | Selection | Yes | `writing_grading`, `speaking_grading`, `video_generation` | +| `status` | Selection | Yes | `pending`, `in_progress`, `completed`, `error`. Default: `pending` | +| `user_id` | Many2one(`res.users`) | No | Requesting user | +| `evaluation_id` | Many2one(`encoach.evaluation`) | No | Related evaluation record | +| `input_data` | Text (JSON) | No | Serialized input for the AI task | +| `result_data` | Text (JSON) | No | Serialized result from the AI task | +| `error_message` | Text | No | Error details if failed | +| `created_at` | Datetime | Yes | Job creation time | +| `completed_at` | Datetime | No | Job completion time | +| `retry_count` | Integer | No | Number of retries. Default: 0 | + +### 3.4 `encoach.elai.avatar` (new) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | Char | Yes | Avatar display name | +| `avatar_code` | Char | Yes | ELAI avatar code | +| `avatar_url` | Char | No | Avatar preview image URL | +| `gender` | Selection | No | `male`, `female` | +| `canvas` | Char | No | ELAI canvas ID | +| `voice_id` | Char | No | ELAI voice ID | +| `voice_provider` | Char | No | Voice provider name | + +### 3.5 Exam `parts` JSON Structure + +Identical to v1. See v1 document Section 3.6.1 for the full JSON schema for Reading, Listening, Writing, Speaking, and Level exam parts. + +### 3.6 Evaluation `result` JSON Structure + +Identical to v1. See v1 document Section 3.10.1 for the writing and speaking grading result schemas. + +--- + +## 4. REST API Specification + +All endpoints from v1 remain unchanged in path and request/response format. The key difference is that endpoints previously marked as "proxy to ielts-be" are now handled directly by Odoo. + +> **Note:** For the full specification of all non-AI endpoints, refer to Section 4 of the v1 document. They are identical: Auth (4.1), Users (4.2), Registration/Codes (4.3), Entities (4.12), Groups (4.11), Roles (4.13), Permissions (4.14), Invites (4.7), Codes (4.8), Assignments (4.10), Sessions (4.11), Stats (4.12), Payments (4.15), Packages (4.16), Discounts (4.17), Tickets (4.18), Storage (4.19), Approval Workflows (4.21). + +The following endpoints were previously "proxy to ielts-be" and are now handled directly by Odoo: + +### 4.1 Exam Generation Endpoints + +#### `GET /api/exam/reading/{passage}` + +Generate a reading passage using AI. + +**Path params:** `passage` = `1` | `2` | `3` + +**Query params:** `topic` (optional), `word_count` (optional, default ~500) + +**Response (200):** +```json +{ + "title": "The Impact of Urbanization", + "text": "Full passage text..." +} +``` + +**Implementation:** Call OpenAI GPT-4o with passage-specific prompt (see Section 6.3.1). + +#### `POST /api/exam/reading/` + +Generate reading exercises from a passage. + +**Request:** +```json +{ + "text": "passage text", + "exercises": [ + { "type": "multipleChoice", "quantity": 5 }, + { "type": "trueFalse", "quantity": 4 }, + { "type": "fillBlanks", "quantity": 3, "num_random_words": 1, "max_words": 3 } + ], + "difficulty": ["B1", "B2"] +} +``` + +**Response (200):** `{ "exercises": [...] }` + +#### `GET /api/exam/listening/{section}` + +Generate a listening dialog/monologue. + +**Path params:** `section` = `1` | `2` | `3` | `4` + +**Query params:** `topic` (optional), `difficulty` (optional) + +**Response (200):** +```json +{ + "dialog": { + "conversation": [ + { "name": "Sarah", "gender": "female", "text": "Hello..." }, + { "name": "James", "gender": "male", "text": "Hi..." } + ] + } +} +``` + +Sections 1 and 3 return `conversation` (dialog). Sections 2 and 4 return `monologue`. + +#### `POST /api/exam/listening/media` + +Generate MP3 audio from a dialog/monologue using AWS Polly TTS. + +**Request:** +```json +{ + "conversation": [ + { "name": "Sarah", "gender": "female", "text": "Hello...", "voice": "Ruth" } + ] +} +``` + +Or for monologue: +```json +{ + "monologue": "Full monologue text..." +} +``` + +**Response:** MP3 binary data (`Content-Type: audio/mpeg`) + +**Implementation:** Call AWS Polly with neural engine (see Section 6.4). + +#### `POST /api/exam/listening/transcribe` + +Transcribe audio using Whisper. + +**Request:** Multipart form with `audio` file + +**Response (200):** Dialog object (conversation or monologue text) + +**Implementation:** Run Whisper model locally, then use GPT-4o to clean overlapping segments (see Section 6.5). + +#### `POST /api/exam/listening/instructions` + +Generate MP3 for listening instructions text. + +**Request:** `{ "text": "instructions text" }` + +**Response:** MP3 binary data + +#### `POST /api/exam/listening/` + +Generate listening exercises from a dialog. + +**Request:** +```json +{ + "text": "transcript text", + "exercises": [ + { "type": "multipleChoice", "quantity": 3 }, + { "type": "writeBlanksFill", "quantity": 5 } + ], + "difficulty": ["B1"] +} +``` + +**Response (200):** `{ "exercises": [...] }` + +#### `GET /api/exam/speaking/{task}` + +Generate a speaking task prompt. + +**Path params:** `task` = `1` | `2` | `3` + +**Query params:** `topic`, `first_topic`, `second_topic`, `difficulty` + +**Response (200):** Speaking part content (prompts array for Part 1/3, text block for Part 2) + +#### `POST /api/exam/speaking/media` + +Generate AI avatar video using ELAI. + +**Request:** +```json +{ + "text": "Question text to speak", + "avatar": "avatar_code" +} +``` + +**Response (200):** `{ "status": "STARTED", "result": null }` (async -- poll for completion) + +#### `GET /api/exam/speaking/media/{vid_id}` + +Poll for video generation status. + +**Response (200):** +```json +{ + "status": "COMPLETED", + "result": "https://elai-video-url..." +} +``` + +Possible statuses: `STARTED`, `IN_PROGRESS`, `COMPLETED`, `ERROR` + +#### `GET /api/exam/speaking/avatars` + +List available ELAI avatars. + +**Response (200):** `[ { "name": "...", "avatar_code": "...", "avatar_url": "...", "gender": "..." } ]` + +#### `GET /api/exam/writing/{task}` + +Generate a writing task prompt. + +**Path params:** `task` = `1` | `2` + +**Query params:** `topic`, `difficulty` + +**Response (200):** Writing task prompt text + +#### `POST /api/exam/writing/{task}/attachment` + +Generate an academic writing Task 1 with image attachment. + +**Request:** Multipart form with `file` (chart/image) and optional `difficulty` + +**Response (200):** Writing task prompt with image analysis + +#### `POST /api/exam/level/` + +Generate level test exercises. + +**Request:** +```json +{ + "exercises": [ + { "type": "multipleChoice", "quantity": 10, "difficulty": "B1" }, + { "type": "fillBlanks", "quantity": 5, "text_size": 200, "topic": "education" } + ], + "difficulty": ["A2", "B1", "B2"] +} +``` + +**Response (200):** Generated exercises + +#### `GET /api/exam/level/` + +Get a pre-built level exam. + +#### `GET /api/exam/level/utas` + +Get a UTAS-format level exam. + +#### `POST /api/exam/level/import/` + +Import a level exam from file. + +**Request:** Multipart form with `exercises` and optional `solutions` files + +**Response (200):** Parsed exam object + +#### `POST /api/exam/level/custom/` + +Generate a custom level exam from JSON specification. + +#### `POST /api/exam/reading/import` + +Import a reading exam from Word/Excel file. + +**Request:** Multipart form with `exercises` and optional `solutions` files + +**Response (200):** Parsed exam object + +#### `POST /api/exam/listening/import` + +Import a listening exam from file. + +### 4.2 Grading Endpoints + +#### `POST /api/evaluate/writing` + +Submit a writing answer for AI grading. + +**Request:** +```json +{ + "userId": 1, + "sessionId": 20, + "exerciseId": "uuid", + "question": "Write about...", + "answer": "Student's essay text...", + "task": 1, + "attachment": "optional-image-url" +} +``` + +**Response (200):** `{ "ok": true }` (grading happens asynchronously) + +**Implementation:** Creates `encoach.evaluation` and `encoach.ai.job`, then runs grading in a background thread (see Section 6.2). + +#### `POST /api/evaluate/speaking` + +Submit speaking audio for AI grading. + +**Request:** Multipart form with `userId`, `sessionId`, `exerciseId`, `task`, and audio files (`audio_1`, `audio_2`, etc.) with corresponding `question_N` fields. + +**Response (200):** `{ "ok": true }` (grading happens asynchronously) + +**Implementation:** Whisper transcribes each audio, then GPT-4o grades the transcript (see Section 6.2). + +#### `POST /api/evaluate/interactiveSpeaking` + +Submit interactive speaking (multiple Q&A pairs) for grading. + +**Request:** Same as speaking but with `question_N` and `audio_N` pairs. + +#### `GET /api/evaluate/{sessionId}/{exerciseId}` + +Poll for evaluation result. + +**Response (200):** +```json +{ + "status": "completed", + "result": { "...grading result..." } +} +``` + +#### `POST /api/grading/multiple` + +Grade multiple short-answer exercises. + +**Request:** +```json +{ + "text": "passage text", + "questions": ["Q1", "Q2"], + "answers": ["A1", "A2"] +} +``` + +**Response (200):** `{ "exercises": [{ "id": "...", "correct": true, "correct_answer": "..." }] }` + +**Implementation:** GPT-4o evaluates each answer against the passage. + +#### `POST /api/exam/grade/summary` + +Generate a grading summary for a full exam session. + +**Request:** +```json +{ + "sections": [ + { "code": "reading", "name": "Reading", "grade": 6.5 }, + { "code": "writing", "name": "Writing", "grade": 7.0 } + ] +} +``` + +**Response (200):** +```json +{ + "sections": [ + { + "code": "reading", + "name": "Reading", + "grade": 6.5, + "evaluation": "Detailed evaluation text...", + "suggestions": "Improvement suggestions...", + "bullet_points": ["Focus on skimming", "Practice time management"] + } + ] +} +``` + +### 4.3 Training Endpoints + +#### `POST /api/training` + +Generate personalized training content. + +**Request:** +```json +{ + "userID": 1, + "stats": [{ "exam_id": "...", "date": 1710000000, "performance_comment": "...", "detailed_summary": "..." }] +} +``` + +**Response (200):** `{ "id": "training-record-id" }` + +**Implementation:** Uses FAISS to find relevant tips, GPT to generate personalized recommendations (see Section 6.6). + +#### `POST /api/training/tips` + +Fetch contextual tips. + +**Request:** +```json +{ + "context": "reading passage about climate change", + "question": "What does the author suggest?", + "answer": "student's wrong answer", + "correct_answer": "the correct answer" +} +``` + +**Response (200):** `{ "tips": "Personalized tip text..." }` + +#### `POST /api/transcribe` + +Transcribe audio file. + +**Request:** Multipart form with audio file + +**Response (200):** Transcript text or dialog object + +#### `POST /api/batch_users` + +Bulk import users. + +**Request:** `{ "makerID": "admin-id", "users": [{ ...userDTO }] }` + +**Response (200):** `{ "ok": true }` + +**Implementation:** In v1 this was handled by ielts-be (which imported into Firebase Auth + MongoDB). In v2, Odoo creates users directly in `res.users`. No Firebase import needed. + +--- + +## 5. Authentication & Authorization + +Identical to v1 document Section 5. Summary: + +- **Recommended:** Odoo native auth with JWT tokens for API access +- **7 user roles** mapped to Odoo security groups with `ir.rule` record rules: student, teacher, corporate, admin, developer, agent, mastercorporate + +--- + +## 6. AI/ML Services Integration + +This is the new Section 6, replacing v1's "ielts-be Integration Specification." Odoo calls all external AI services directly. + +### 6.1 External Service Overview + +| Service | Purpose | API Type | Auth | Odoo Module | +|---------|---------|----------|------|-------------| +| **OpenAI GPT-4o** | Grading, content generation, text correction | REST API | API Key (`Authorization: Bearer`) | `encoach_ai` | +| **OpenAI GPT-3.5-turbo** | Secondary tasks (fixed text, perfect answers, summaries) | REST API | Same key | `encoach_ai` | +| **OpenAI Whisper** | Speech-to-text transcription | **Local model** (Python library) | N/A | `encoach_ai_media` | +| **AWS Polly** | Text-to-speech for listening exams | AWS SDK (boto3) | AWS Access Key + Secret | `encoach_ai_media` | +| **ELAI** | AI avatar video generation for speaking | REST API | Bearer token | `encoach_ai_media` | +| **GPTZero** | AI-generated text detection for writing | REST API | API Key (`x-api-key`) | `encoach_ai_grading` | +| **FAISS** | Semantic search over training tips | **Local library** (Python) | N/A | `encoach_training` | +| **sentence-transformers** | Embedding generation for FAISS | **Local model** (Python) | N/A | `encoach_training` | + +### 6.2 Background Task Architecture + +AI grading operations (writing and speaking) take 10-60 seconds. They must run asynchronously. + +**Recommended approach:** Use Python threading within Odoo or the OCA `queue_job` module. + +**Flow:** + +1. Frontend calls `POST /api/evaluate/writing` (or speaking) +2. Odoo controller creates `encoach.evaluation` record with `status = 'pending'` +3. Odoo controller creates `encoach.ai.job` record and launches a background thread +4. Controller returns `200 OK` immediately to the frontend +5. Background thread: + a. Calls OpenAI GPT-4o for grading (and GPTZero for AI detection in parallel) + b. Updates `encoach.evaluation` with `status = 'completed'` and `result` JSON + c. Updates `encoach.ai.job` with `status = 'completed'` +6. Frontend polls `GET /api/evaluate/{sessionId}/{exerciseId}` until status is `completed` + +**Threading pattern (Odoo-compatible):** + +```python +import threading +from odoo import api, SUPERUSER_ID + +def _run_grading_in_background(dbname, evaluation_id, input_data): + with api.Environment.manage(): + registry = odoo.registry(dbname) + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + service = env['encoach.ai.grading'] + service.execute_grading(evaluation_id, input_data) + +# In the controller: +thread = threading.Thread( + target=_run_grading_in_background, + args=(request.env.cr.dbname, evaluation.id, input_data) +) +thread.start() +``` + +**Alternative:** Use OCA `queue_job` for production-grade async processing with retry, monitoring, and worker management. + +### 6.3 OpenAI Integration + +#### 6.3.1 Client Configuration + +Create a service class `EncoachOpenAIService` in `encoach_ai`: + +| Parameter | Value | +|-----------|-------| +| **Library** | `openai` (Python package) | +| **Client** | `AsyncOpenAI(api_key=api_key)` or synchronous `OpenAI` | +| **API Key** | Odoo system parameter `encoach.openai_api_key` | +| **Response format** | `response_format={"type": "json_object"}` | + +#### 6.3.2 Models Used + +| Task | Model | Temperature | +|------|-------|-------------| +| Writing grading | `gpt-4o` | 0.1 | +| Speaking grading | `gpt-4o` | 0.1 | +| Short answer grading | `gpt-4o` | 0.1 | +| Fixed text / perfect answer | `gpt-3.5-turbo` | 0.1 | +| Grading summary | `gpt-3.5-turbo` | 0.1 | +| Reading passage generation | `gpt-4o` | 0.7 | +| Listening dialog generation | `gpt-4o` | 0.7 | +| Writing task generation | `gpt-4o` | 0.7 | +| Speaking task generation | `gpt-4o` | 0.7 | +| Level exercise generation | `gpt-4o` | 0.7 | +| Training tips | `gpt-4o` | 0.2 | +| Whisper overlap cleanup | `gpt-4o` | 0.1 | + +#### 6.3.3 Writing Grading Prompts + +**System message:** + +`You are a helpful assistant designed to output JSON on this format: {template}` + +Where `{template}` is: + +```json +{ + "comment": "comment about student's response quality", + "overall": 0.0, + "task_response": { + "Task Achievement": { "grade": 0.0, "comment": "..." }, + "Coherence and Cohesion": { "grade": 0.0, "comment": "..." }, + "Lexical Resource": { "grade": 0.0, "comment": "..." }, + "Grammatical Range and Accuracy": { "grade": 0.0, "comment": "..." } + } +} +``` + +**User message (Task 2):** + +`Evaluate the given Writing Task {task} response based on the IELTS grading system, ensuring a strict assessment that penalizes errors. Deduct points for deviations from the task, and assign a score of 0 if the response fails to address the question. Additionally, provide a detailed commentary highlighting both strengths and weaknesses in the response.\nQuestion: "{question}"\nAnswer: "{answer}"` + +**User message (Task 1 General):** + +Same as Task 2 plus: `Refer to the parts of the letter as: "Greeting Opener", "bullet 1", "bullet 2", "bullet 3", "closer (restate the purpose of the letter)", "closing greeting"` + +**User message (Task 1 Academic with image):** + +The image is sent as base64 in the message content using the vision API. + +**Additional parallel tasks for writing grading:** + +| Task | Model | Prompt | +|------|-------|--------| +| Perfect answer | `gpt-3.5-turbo` | `Write a perfect answer for this IELTS writing task: "{question}"` | +| Fixed text | `gpt-3.5-turbo` | `Fix the grammatical and spelling errors in this text, keeping the original meaning: "{answer}"` | +| AI detection | GPTZero API | See Section 6.7 | + +#### 6.3.4 Speaking Grading Prompts + +**System message:** + +`You are a helpful assistant designed to output JSON on this format: {template}` + +Where `{template}` is: + +```json +{ + "comment": "extensive comment about answer quality", + "overall": 0.0, + "task_response": { + "Fluency and Coherence": { "grade": 0.0, "comment": "extensive comment..." }, + "Lexical Resource": { "grade": 0.0, "comment": "..." }, + "Grammatical Range and Accuracy": { "grade": 0.0, "comment": "..." }, + "Pronunciation": { "grade": 0.0, "comment": "..." } + } +} +``` + +**User message:** + +`Evaluate the given Speaking Part {task} response based on the IELTS grading system, ensuring a strict assessment that penalizes errors. Deduct points for deviations from the task, and assign a score of 0 if the response fails to address the question. Additionally, provide detailed commentary highlighting both strengths and weaknesses in the response.` + +Followed by question/answer pairs from transcription. + +**Task-specific instructions:** + +- Task 1: `Address the student as "you". If the answers are not 2 or 3 sentences long, warn the student that they should be.` +- Task 2: `Address the student as "you"` +- Task 3: `Address the student as "you" and pay special attention to coherence between the answers.` + +**Speaking grading flow:** + +1. Whisper transcribes each audio file +2. GPT-4o grades the full transcript against the rubric +3. GPT-3.5-turbo generates fixed text and perfect answer (for Task 2) +4. Results combined into the evaluation record + +#### 6.3.5 Reading Passage Generation Prompts + +**System message:** + +`You are a helpful assistant designed to output JSON on this format: {"title": "title of the text", "text": "generated text"}` + +**User message (Passage 1):** + +`Generate an extensive text for IELTS Reading Passage 1, of at least {word_count} words, on the topic of "{topic}". The passage should offer a substantial amount of information relevant to the chosen subject matter. It should be fairly easy and consist of multiple paragraphs. Make sure that the generated text does not contain forbidden subjects in muslim countries.` + +**User message (Passage 2):** + +Same structure but: `fairly hard and consist of multiple paragraphs` + +**User message (Passage 3):** + +Same structure but: `very hard, present different points or theories, cite different sources, and consist of multiple paragraphs` + +#### 6.3.6 Listening Dialog Generation Prompts + +**Section 1 (conversation, 2 people):** + +System: `{"conversation": [{"name": "name", "gender": "gender", "text": "text"}]}` + +User: `Compose an authentic conversation between two individuals on the topic of "{topic}". Please include random names and genders. Include misleading discourse (dates, colors, etc.) and spelling of names. Make sure that the generated conversation does not contain forbidden subjects in muslim countries.` + +**Section 2 (monologue, social):** + +System: `{"monologue": "monologue"}` + +User: `Generate a comprehensive monologue set in the social context of "{topic}". Make sure that the generated monologue does not contain forbidden subjects in muslim countries.` + +**Section 3 (conversation, up to 4 people):** + +User: `Compose an authentic and elaborate conversation between up to four individuals on the topic of "{topic}". Please include random names and genders. Make sure that the generated conversation does not contain forbidden subjects in muslim countries.` + +**Section 4 (monologue, academic):** + +User: `Generate a comprehensive and complex monologue on the academic subject of "{topic}". Make sure that the generated monologue does not contain forbidden subjects in muslim countries.` + +#### 6.3.7 Writing Task Generation Prompts + +**General Task 1:** + +`Craft a prompt for an IELTS Writing Task 1 General Training exercise that instructs the student to compose a letter based on the topic of "{topic}" of {difficulty} CEFR level difficulty. The prompt should end with "In the letter you should" followed by 3 bullet points. Make sure it does not contain forbidden subjects in muslim countries.` + +**General Task 2:** + +`Craft a comprehensive question of {difficulty} CEFR level difficulty like the ones for IELTS Writing Task 2 General Training that directs the candidate to delve into an in-depth analysis of contrasting perspectives on the topic of "{topic}". The question should lead to an answer with either "theories", "complicated information" or be "very descriptive" on the topic.` + +**Academic Task 1 (with image):** + +`Analyze the uploaded image and create a detailed IELTS Writing Task 1 Academic prompt. Describe the visual type, context, and create a prompt at {difficulty} CEFR level.` + +#### 6.3.8 Speaking Task Generation Prompts + +**Part 1:** + +`Craft 5 simple and single questions of easy difficulty for IELTS Speaking Part 1 that encourages candidates to delve deeply into personal experiences, preferences, or insights on the topic of "{first_topic}" and the topic of "{second_topic}". The questions should lead to the usage of 4 verb tenses (present perfect, present, past and future). Make sure that the generated question does not contain forbidden subjects in muslim countries.` + +**Part 2:** + +`Create a question of medium difficulty for IELTS Speaking Part 2 that encourages candidates to narrate a personal experience or story related to the topic of "{topic}". Include 3 prompts that guide the candidate. The prompts must not be questions. Also include a suffix like the ones in the IELTS exams that start with "And explain why". Make sure that the generated question does not contain forbidden subjects in muslim countries.` + +**Part 3:** + +`Formulate a set of 5 single questions of hard difficulty for IELTS Speaking Part 3 that encourage candidates to engage in a meaningful discussion on the topic of "{topic}". They must be 1 single question each and not be double-barreled questions. Make sure that the generated question does not contain forbidden subjects in muslim countries.` + +#### 6.3.9 Level Exercise Generation Prompts + +**Multiple Choice:** + +`Generate {quantity} multiple choice questions of 4 options for an english level exam of {difficulty} CEFR level. Ensure that the questions cover a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence structure, and punctuation. Make sure every question only has 1 correct answer.` + +**Fill Blanks:** + +`Generate a text of at least {size} words about the topic {topic}. From the generated text choose exactly {quantity} words (cannot be sequential words), replace each with {{id}}, and generate a JSON object containing: the modified text, solutions array, words array with four options per blank.` + +#### 6.3.10 Retry and Validation Logic + +- Retry up to 2 times if response contains blacklisted words or missing required fields +- Blacklisted words include topics related to religion, politics, explicit content, and culturally sensitive subjects (stored in constants) +- Token limit: `4097 - input_token_count - 300` for max_tokens +- All responses expected in JSON format via `response_format={"type": "json_object"}` + +### 6.4 AWS Polly Integration (TTS) + +Implemented in `encoach_ai_media` module. + +| Parameter | Value | +|-----------|-------| +| **Library** | `boto3` / `aioboto3` | +| **Service** | `polly` | +| **Engine** | `neural` | +| **Output format** | `mp3` | +| **Auth** | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (Odoo system parameters) | + +**Available voices:** + +| Voice | Gender | Accent | +|-------|--------|--------| +| Danielle | Female | US | +| Gregory | Male | US | +| Kevin | Male | US | +| Ruth | Female | US | +| Stephen | Male | US | +| Arthur | Male | GB | +| Olivia | Female | GB | +| Ayanda | Female | ZA | +| Aria | Female | NZ | +| Kajal | Female | IN | +| Niamh | Female | IE | + +**Conversation audio generation:** + +1. For each dialog line, call Polly `synthesize_speech()` with the assigned voice +2. Chunk text at sentence boundaries if > 3000 characters +3. Concatenate all MP3 segments +4. Append final message: `"This audio recording, for the listening exercise, has finished."` (voice: Stephen) + +**Monologue audio generation:** + +1. Select a random voice +2. Chunk text at sentence boundaries if > 3000 characters +3. Synthesize each chunk and concatenate + +### 6.5 Whisper Integration (STT) + +Implemented in `encoach_ai_media` module. + +| Parameter | Value | +|-----------|-------| +| **Library** | `openai-whisper` (local Python package) | +| **Model size** | `base` (~1 GB, or configurable) | +| **Model instances** | 4 (for parallel transcription) | +| **Thread pool** | `ThreadPoolExecutor(max_workers=4)` | +| **Audio resampling** | 16 kHz, normalized | +| **Chunk size** | 30 seconds (480,000 samples) with 1/4 overlap | +| **Options** | `fp16=False`, `language='English'`, `verbose=False` | +| **Retries** | 3 attempts via tenacity | + +**Long audio handling:** + +1. Split audio into 30-second chunks with 25% overlap +2. Transcribe each chunk independently +3. Use GPT-4o to remove duplicated words at chunk boundaries +4. Join cleaned segments into final transcript + +**Audio-to-dialog conversion:** + +After transcription, use GPT-4o to determine whether the transcript is a conversation or monologue and output structured JSON: + +``` +You are a helpful assistant designed to output JSON on either one of these formats: +1 - {"dialog": [{"name": "name", "gender": "gender", "text": "text"}]} +2 - {"dialog": "text"} + +A transcription of an audio file will be provided to you. Based on that transcription you will need to determine whether the transcription is a conversation or a monologue. If it is a conversation, output format 1. If it is a monologue, output format 2. +``` + +### 6.6 ELAI Integration (AI Video) + +Implemented in `encoach_ai_media` module. + +| Parameter | Value | +|-----------|-------| +| **Base URL** | `https://apis.elai.io/api/v1/videos` | +| **Auth** | Bearer token (Odoo system parameter `encoach.elai_token`) | + +**Video generation flow:** + +1. `POST /api/v1/videos` -- Create video with avatar, text, voice +2. `POST /api/v1/videos/render/{video_id}` -- Start rendering +3. Poll `GET /api/v1/videos/{video_id}` -- Check status (`ready`, `failed`, or in progress) +4. Return video URL when `ready` + +**Avatar configuration:** Stored in `encoach.elai.avatar` model (avatar codes, voice IDs, voice providers). + +### 6.7 GPTZero Integration (AI Detection) + +Implemented in `encoach_ai_grading` module. + +| Parameter | Value | +|-----------|-------| +| **Endpoint** | `https://api.gptzero.me/v2/predict/text` | +| **Auth** | `x-api-key` header (Odoo system parameter `encoach.gptzero_api_key`) | + +**Request:** +```json +{ + "document": "student's writing text", + "version": "", + "multilingual": false +} +``` + +**Response fields used:** +- `class_probabilities` -- probability scores for human/AI/mixed +- `predicted_class` -- `human`, `ai`, or `mixed` +- `sentences[].highlight_sentence_for_ai` -- boolean per sentence + +**Result stored in evaluation:** `"ai_detection": { "probability": 0.12, "predicted_class": "human" }` + +### 6.8 FAISS + Sentence-Transformers (Training) + +Implemented in `encoach_training` module. + +| Parameter | Value | +|-----------|-------| +| **Embeddings model** | `sentence-transformers/all-MiniLM-L6-v2` | +| **Index type** | `faiss.IndexFlatL2` (one per category) | +| **Top-K results** | 5 | + +**Categories:** + +| Category | Description | +|----------|-------------| +| `ct_focus` | Critical thinking focus tips | +| `language_for_writing` | Language for writing tips | +| `reading_skill` | Reading skills tips | +| `strategy` | Test strategy tips | +| `writing_skill` | Writing skills tips | +| `word_link` | Word linking tips | +| `word_partners` | Word partner/collocation tips | + +**Index files:** Store as Odoo attachments or on disk: +- `{category}_tips_index.faiss` -- FAISS index file per category +- `tips_metadata.pkl` -- Metadata mapping index positions to tip content + +**Query flow:** + +1. Encode user query using sentence-transformers: `embedding = model.encode([query])` +2. Search FAISS index: `distances, indices = index.search(embedding, top_k=5)` +3. Retrieve tip content from metadata +4. Pass tips + user performance data to GPT for personalized recommendations + +**Training content generation flow:** + +1. Receive user's exam stats (performance comments, detailed summaries) +2. Use GPT to identify weak areas and generate training queries +3. For each query, search FAISS index to find relevant tips +4. Use GPT to generate personalized recommendations based on tips + performance +5. Store result in `encoach.training` + +### 6.9 Topic and Content Constants + +Maintain in `encoach_ai` module as constants or configuration records: + +**Topics for content generation:** + +- `TOPICS` -- General IELTS topics (education, technology, environment, health, etc.) +- `TWO_PEOPLE_SCENARIOS` -- Listening Section 1 scenarios +- `SOCIAL_MONOLOGUE_CONTEXTS` -- Listening Section 2 contexts +- `FOUR_PEOPLE_SCENARIOS` -- Listening Section 3 scenarios +- `ACADEMIC_SUBJECTS` -- Listening Section 4 subjects + +**Difficulty levels:** `["A1", "A2", "B1", "B2", "C1", "C2"]` + +**Blacklisted words:** List of culturally sensitive terms that trigger retry if found in AI-generated content (religion, politics, explicit content, etc.) + +### 6.10 Python Dependencies + +Add to the Odoo server's Python environment: + +| Package | Version | Purpose | +|---------|---------|---------| +| `openai` | >= 1.50 | OpenAI API client (GPT-4o, GPT-3.5) | +| `openai-whisper` | latest | Local Whisper model for STT | +| `boto3` | >= 1.34 | AWS SDK for Polly TTS | +| `faiss-cpu` | >= 1.7 | FAISS vector search | +| `sentence-transformers` | >= 3.0 | Embeddings for FAISS | +| `httpx` | >= 0.27 | HTTP client for ELAI, GPTZero | +| `tiktoken` | >= 0.7 | Token counting for OpenAI | +| `librosa` | >= 0.10 | Audio processing for Whisper | +| `soundfile` | >= 0.12 | Audio file I/O | +| `numpy` | >= 1.26 | Numerical operations | +| `tenacity` | >= 8.2 | Retry logic for API calls | +| `torch` | >= 2.0 | Required by Whisper and sentence-transformers | + +**Server requirements for Whisper:** The Odoo server needs at least 4 GB RAM for the Whisper base model and sentence-transformers model. Consider running Whisper on a GPU for better performance, or use the OpenAI Whisper API instead of local inference. + +### 6.11 Configuration (Odoo System Parameters) + +| Parameter Key | Description | Example | +|--------------|-------------|---------| +| `encoach.openai_api_key` | OpenAI API key | `sk-...` | +| `encoach.aws_access_key_id` | AWS access key | `AKIA...` | +| `encoach.aws_secret_access_key` | AWS secret key | `wJal...` | +| `encoach.elai_token` | ELAI API token | `Bearer ...` | +| `encoach.gptzero_api_key` | GPTZero API key | `...` | +| `encoach.whisper_model_size` | Whisper model size | `base` (or `small`, `medium`, `large`) | +| `encoach.whisper_workers` | Number of Whisper worker threads | `4` | +| `encoach.grading_temperature` | Temperature for grading prompts | `0.1` | +| `encoach.generation_temperature` | Temperature for content generation | `0.7` | +| `encoach.tips_temperature` | Temperature for training tips | `0.2` | + +--- + +## 7. Payment Integration + +Identical to v1 document Section 7. Summary: + +- **Stripe:** Checkout session creation, webhook handling, subscription extension +- **PayPal:** Order creation, capture, subscription update +- **Paymob:** Intention creation, transaction webhook verification +- Subscription logic: extend `subscriptionExpirationDate` on successful payment; propagate to entity members for corporate + +--- + +## 8. Business Rules & Workflows + +Identical to v1 document Section 8, with one addition: + +### 8.1-8.7 (Unchanged from v1) + +See v1 for: Registration flow, Subscription management, Entity licensing, Assignment lifecycle, Grading polling, Corporate payment activation, User change propagation. + +### 8.8 AI Content Moderation (New) + +All AI-generated content must be validated before being returned to the user: + +1. **Blacklist check:** Scan generated text against the blacklisted words list +2. **Retry logic:** If blacklisted words are found, regenerate with the same prompt (up to 2 retries) +3. **JSON validation:** Verify the AI response is valid JSON with all required fields +4. **Score validation:** For grading, verify all scores are between 0.0 and 9.0 +5. **Content length:** For passages, verify minimum word count is met + +### 8.9 Batch User Import (Updated) + +In v1, batch import was proxied to ielts-be (which imported into Firebase Auth). In v2: + +1. Admin uploads CSV/Excel with user data +2. Odoo parses the file and creates `res.users` records directly +3. Generates temporary passwords and sends welcome emails +4. Creates registration codes and group assignments +5. No Firebase Auth involvement + +--- + +## 9. Data Migration Plan + +### 9.1 Migration Strategy + +Perform a one-time data migration from both data sources: + +1. **MongoDB** (used by ielts-ui) -> PostgreSQL +2. **MongoDB** (used by ielts-be) -> PostgreSQL (evaluation records, training data) +3. **Firebase Auth** users -> Odoo `res.users` + +### 9.2 Collection-to-Model Mapping + +Identical to v1 Section 9.2, with additional ielts-be collections: + +| ielts-be MongoDB Collection | Odoo Model | Notes | +|----------------------------|------------|-------| +| `evaluation` | `encoach.evaluation` | Grading records with result JSON | +| `training` | `encoach.training` | Training content | + +### 9.3 Additional Migration: AI Assets + +| Asset | Source | Target | +|-------|--------|--------| +| FAISS index files | ielts-be `./faiss/` directory | Odoo server filesystem or `ir.attachment` | +| Tips metadata | `tips_metadata.pkl` | `encoach.training.tip` model records | +| Tips source data | `pathways_2_rw_with_ids.json` | `encoach.training.tip` model records | +| Avatar config | `conf.json`, `avatars.json` | `encoach.elai.avatar` model records | +| Whisper model | Downloaded at runtime | Odoo server filesystem | + +### 9.4 Migration Script Requirements & Import Order + +Identical to v1 Section 9.3 and 9.4, with the addition of: + +22. `encoach.training.tip` (from tips JSON) +23. `encoach.elai.avatar` (from avatar config JSON) +24. FAISS index rebuild (re-embed tips into FAISS after import) + +--- + +## 10. Non-Functional Requirements + +### 10.1-10.6 (Unchanged from v1) + +See v1 for: API response format, CORS, file storage, performance targets, scalability, security. + +### 10.7 AI/ML Performance Requirements (New) + +| Operation | Target Response Time | Notes | +|-----------|---------------------|-------| +| Reading passage generation | < 15s | Single GPT-4o call | +| Listening dialog generation | < 15s | Single GPT-4o call | +| Listening MP3 generation | < 30s | Multiple Polly calls + concatenation | +| Writing task generation | < 10s | Single GPT-4o call | +| Speaking task generation | < 10s | Single GPT-4o call | +| Level exercise generation | < 20s | Multiple GPT-4o calls | +| Writing grading (async) | < 30s total | Parallel: GPT-4o grading + GPTZero + perfect answer + fixed text | +| Speaking grading (async) | < 60s total | Sequential: Whisper transcription + GPT-4o grading | +| Short answer grading | < 10s | Single GPT-4o call | +| Transcription (1 min audio) | < 15s | Local Whisper | +| FAISS query | < 1s | Local computation | +| Video generation (async) | 1-5 min | ELAI rendering; poll for completion | + +### 10.8 AI/ML Scalability (New) + +- Whisper model requires ~1 GB RAM per instance (4 instances = 4 GB) +- sentence-transformers model requires ~500 MB RAM +- FAISS indices are small (< 100 MB total) +- OpenAI API has rate limits -- implement request queuing if needed +- AWS Polly has a 3000-character limit per request -- chunking is already handled +- Consider GPU acceleration for Whisper if transcription volume is high + +### 10.9 AI/ML Monitoring (New) + +- Log all OpenAI API calls with model, token usage, response time, and cost +- Log all Polly synthesis calls with character count +- Log all ELAI video generation requests with status and duration +- Log all Whisper transcription jobs with audio duration and processing time +- Alert on repeated grading failures (> 3 consecutive errors) +- Track OpenAI API spend against budget thresholds + +### 10.10 Logging, Testing (Unchanged from v1) + +See v1 Sections 10.7 and 10.8. + +--- + +## Appendix A: Exercise Type Reference + +Identical to v1 Appendix A. + +## Appendix B: Grading Rubric Details + +Identical to v1 Appendix B. + +## Appendix C: Environment Variables + +Updated to include all AI/ML service credentials: + +| Variable | Description | Example | +|----------|-------------|---------| +| `ENCOACH_JWT_SECRET` | JWT signing secret | Random 256-bit string | +| `OPENAI_API_KEY` | OpenAI API key | `sk-...` | +| `AWS_ACCESS_KEY_ID` | AWS access key for Polly | `AKIA...` | +| `AWS_SECRET_ACCESS_KEY` | AWS secret key for Polly | `wJal...` | +| `ELAI_TOKEN` | ELAI API bearer token | `...` | +| `GPT_ZERO_API_KEY` | GPTZero API key | `...` | +| `WHISPER_MODEL_SIZE` | Whisper model size | `base` | +| `STRIPE_SECRET_KEY` | Stripe secret key | `sk_live_...` | +| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | `whsec_...` | +| `PAYPAL_CLIENT_ID` | PayPal client ID | `AX...` | +| `PAYPAL_CLIENT_SECRET` | PayPal client secret | `EL...` | +| `PAYPAL_ACCESS_TOKEN_URL` | PayPal OAuth URL | `https://api.paypal.com/v1/oauth2/token` | +| `PAYMOB_API_KEY` | Paymob API key | `ZXlK...` | +| `PAYMOB_SECRET` | Paymob webhook secret | `...` | + +## Appendix D: Glossary + +Identical to v1 Appendix D, with additions: + +| Term | Definition | +|------|-----------| +| **Whisper** | OpenAI's speech-to-text model, run locally on the Odoo server | +| **Polly** | AWS text-to-speech service for generating listening exam audio | +| **ELAI** | AI video generation service for creating speaking exam avatar videos | +| **GPTZero** | AI text detection service for identifying AI-generated writing submissions | +| **FAISS** | Facebook AI Similarity Search -- vector search library for finding relevant training tips | +| **sentence-transformers** | Python library for generating text embeddings used with FAISS | + +## Appendix E: Custom Odoo Module Summary + +| Module | Complexity | Key Models | Key Services | External APIs | +|--------|------------|------------|-------------- |---------------| +| `encoach_core` | Medium | User extensions, Entity, Role, Permission, Code, Invite | Registration, entity management | -- | +| `encoach_exam` | High | Exam (5 modules), Exercise structures | Exam CRUD, import/export | -- | +| `encoach_classroom` | Low | Group | Group management | -- | +| `encoach_assignment` | Medium | Assignment | Assignment lifecycle | -- | +| `encoach_stats` | Medium | Session, Stat | Stats tracking, PDF export | -- | +| `encoach_evaluation` | Medium | Evaluation, AI Job | Async grading coordination | -- | +| `encoach_training` | Medium | Training, Training Tip | FAISS search, tip generation | -- | +| `encoach_subscription` | Medium | Package, Payment, Subscription Payment, Discount | Subscription management | Stripe, PayPal, Paymob | +| `encoach_registration` | Medium | Code, Invite | Multi-path registration | -- | +| `encoach_ticket` | Low | Ticket | Ticket CRUD | -- | +| `encoach_ai` | **Very High** | -- | OpenAI client, prompt management, retry logic | OpenAI API | +| `encoach_ai_grading` | High | -- | Writing grading, speaking grading, short answer grading | OpenAI, GPTZero | +| `encoach_ai_generation` | High | -- | Reading/listening/writing/speaking/level generation | OpenAI | +| `encoach_ai_media` | High | ELAI Avatar | Polly TTS, Whisper STT, ELAI video | AWS Polly, ELAI, Whisper (local) | +| `encoach_api` | High | -- | REST JSON controllers (~60 endpoints) | -- | diff --git a/docs/UTAS_MASTER_PLAN.md b/docs/UTAS_MASTER_PLAN.md new file mode 100644 index 00000000..65b0615b --- /dev/null +++ b/docs/UTAS_MASTER_PLAN.md @@ -0,0 +1,727 @@ +# EnCoach -- UTAS University Deployment Master Plan + +**Document Version:** 1.0 +**Date:** March 16, 2026 +**Classification:** Confidential -- EnCoach & UTAS Internal +**Prepared by:** EnCoach Engineering Team + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Project Scope and Objectives](#2-project-scope-and-objectives) +3. [System Architecture](#3-system-architecture) +4. [Feature Modules Breakdown](#4-feature-modules-breakdown) +5. [Development Phases](#5-development-phases) +6. [University Integration Plan](#6-university-integration-plan) +7. [Master Timeline](#7-master-timeline) +8. [Testing and Rollout Strategy](#8-testing-and-rollout-strategy) +9. [Team and Responsibilities](#9-team-and-responsibilities) +10. [Risk Register](#10-risk-register) +11. [Assumptions and Prerequisites](#11-assumptions-and-prerequisites) + +--- + +## 1. Executive Summary + +EnCoach is deploying a comprehensive AI-powered education platform for the University of Technology and Applied Sciences (UTAS). The platform delivers nine integrated modules spanning self-paced English learning, AI-graded examinations, Mathematics and IT courses with AI-assisted assessment, a full Learning Management System, university SIS integration, institutional whitelabeling, AI virtual tutors, and a support ticketing system. + +### Platform at a Glance + +| Dimension | Detail | +|-----------|--------| +| **Modules** | 9 (English Self Learning, Examinations, Math, IT, LMS, Whitelabeling, Humanisation, SIS Integration, Ticketing) | +| **AI Services** | 6 (OpenAI GPT-4o, Whisper STT, AWS Polly TTS, ELAI Avatars, GPTZero Detection, FAISS Semantic Search) | +| **Backend** | Odoo 19 -- 15 custom modules, 70+ REST API endpoints, PostgreSQL | +| **Frontend** | React 18 SPA -- 55+ pages, TypeScript, Tailwind CSS, shadcn/ui | +| **LMS Engine** | OpenEduCat (Odoo-native modules) | +| **Team** | 1 Solution Architect, 1 Senior Developer (AI-assisted), 1 Full-Stack Developer (AI-assisted) | + +### Key Milestones + +| Milestone | Target Date | +|-----------|-------------| +| Project Kickoff | April 1, 2026 | +| Staging Demo to UTAS | April 24, 2026 | +| English Self Learning + Examinations -- GO LIVE | June 12, 2026 | +| Math + IT + LMS + Ticketing -- GO LIVE | July 24, 2026 | +| Full Platform GO LIVE (SIS, Whitelabeling, Humanisation) | August 21, 2026 | + +### Current State of Development + +The platform is not starting from zero. Significant engineering has already been completed: + +- **Backend (Odoo 19):** 15 custom modules fully developed, covering exams, AI grading, content generation, media services, training, assignments, classrooms, subscriptions, registration, and ticketing. All AI services (OpenAI, Whisper, Polly, ELAI, GPTZero, FAISS) are integrated and configured. The backend is deployed and running on a staging server. +- **Frontend (React):** A complete UI prototype with 55+ pages across student, teacher, and admin portals. Role-based routing, layouts, and forms are built. Currently using mock data -- API integration is the primary remaining work. +- **Infrastructure:** Docker Compose deployment with PostgreSQL, staging environment operational at the current staging server. + +--- + +## 2. Project Scope and Objectives + +### 2.1 Objectives + +1. **Deliver a production-ready AI-powered education platform** for UTAS covering English, Mathematics, and IT subjects. +2. **Integrate with UTAS's existing Student Information System (SIS)** for seamless student enrollment, grade sync, and attendance tracking. +3. **Provide institutional whitelabeling** with UTAS branding, colors, and identity. +4. **Deploy AI virtual tutors** (Humanisation) to enhance the student learning experience across subjects. +5. **Establish a scalable LMS** built on OpenEduCat for course management, timetabling, and academic administration. + +### 2.2 Module Scope + +| # | Module | Description | Acceptance Criteria | +|---|--------|-------------|---------------------| +| 1 | **English Self Learning** | AI-powered IELTS preparation: Reading, Listening, Writing, Speaking practice with AI grading, personalized training tips, and progress tracking. | Students can complete full practice sessions in all 4 skills; AI grading returns scores within 60 seconds; training tips are personalized to weaknesses. | +| 2 | **Examinations** | Full exam lifecycle: AI-generated exam content, timed exam sessions, automated grading, results dashboard, and teacher approval workflows. | Teachers can generate, review, and assign exams; students complete timed exams; grading is automated with band scores and feedback. | +| 3 | **Mathematics** | Math courses and AI-assisted exams: course content delivery, formula-based question generation, automated grading for numerical and short-answer responses. | Students access Math courses; AI generates Math questions at configurable difficulty levels; automated grading handles numerical and short-answer formats. | +| 4 | **Information Technology** | IT courses and AI-assisted exams: course content delivery, conceptual and practical question generation, automated grading for MCQ and short-answer formats. | Students access IT courses; AI generates IT questions across topics; automated grading covers MCQ, true/false, and short answers. | +| 5 | **LMS (OpenEduCat)** | Course management, enrollment, timetabling, attendance tracking, batch management, gradebook, and academic reporting via OpenEduCat Odoo modules. | Admin manages courses, batches, and timetables; teachers record attendance and grades; students view schedules and progress. | +| 6 | **Whitelabeling** | UTAS institutional branding: customizable logo, color palette, fonts, login screen, email templates, and report headers. | Platform displays UTAS branding throughout; branding is configurable via admin settings without code changes. | +| 7 | **Humanisation** | AI-powered virtual tutors using ELAI avatar technology: video-based tutoring, interactive speaking practice, and subject-specific AI guidance. | AI avatars present content and interact with students across subjects; avatar selection is available; videos generate within 2 minutes. | +| 8 | **SIS Integration** | Bidirectional integration with UTAS's existing Student Information System: student enrollment sync, grade export, attendance sync, and course catalog mapping. | Student data syncs from SIS to EnCoach on enrollment; grades and attendance export back to SIS; data is consistent within 15-minute sync cycles. | +| 9 | **Ticketing** | Support ticket system for students and staff: ticket creation, categorization, assignment, status tracking, and resolution workflow. | Users submit tickets; staff can categorize, assign, and resolve; status updates are visible to the reporter. | + +### 2.3 Out of Scope (This Engagement) + +- Mobile native applications (iOS/Android) +- Content creation for Math and IT courses (content to be provided by UTAS or generated via AI) +- UTAS internal network infrastructure changes +- Marketing website / landing page redesign +- Payment gateway integration for UTAS (institutional licensing assumed) + +--- + +## 3. System Architecture + +### 3.1 Architecture Overview + +```mermaid +graph TB + subgraph frontend ["Frontend Layer"] + ReactApp["React 18 SPA
TypeScript + Tailwind + shadcn/ui
55+ pages, role-based routing"] + end + + subgraph backend ["Backend Layer -- Odoo 19"] + API["REST API Controllers
70+ JSON endpoints"] + Core["Core Modules
Users, Entities, Roles, Permissions"] + Exam["Exam Engine
Reading, Listening, Writing, Speaking, Level"] + LMS["OpenEduCat LMS
Courses, Batches, Timetable, Attendance"] + AI["AI Modules
Generation, Grading, Media, Training"] + Sub["Business Modules
Subscriptions, Tickets, Registration"] + end + + subgraph aiServices ["AI Services"] + GPT["OpenAI GPT-4o
Content Generation + Grading"] + Whisper["OpenAI Whisper
Speech-to-Text (local)"] + Polly["AWS Polly
Text-to-Speech (neural)"] + ELAI["ELAI
AI Avatar Video"] + GPTZero["GPTZero
AI Writing Detection"] + FAISS["FAISS + SentenceTransformers
Semantic Search"] + end + + subgraph dataLayer ["Data Layer"] + PG["PostgreSQL 16"] + end + + subgraph external ["External Systems"] + SIS["UTAS SIS
Student Information System"] + end + + ReactApp -->|"JWT Auth + REST API"| API + API --> Core + API --> Exam + API --> LMS + API --> AI + API --> Sub + AI --> GPT + AI --> Whisper + AI --> Polly + AI --> ELAI + AI --> GPTZero + AI --> FAISS + Core --> PG + Exam --> PG + LMS --> PG + Sub --> PG + API <-->|"REST API Sync"| SIS +``` + +### 3.2 Technology Stack + +| Layer | Technology | Details | +|-------|-----------|---------| +| **Frontend** | React 18, TypeScript, Vite 5 | SPA with TanStack Query, React Router v6, Zod validation | +| **UI Framework** | shadcn/ui, Tailwind CSS | Radix UI primitives, responsive design, dark mode capable | +| **Backend** | Odoo 19, Python 3.11 | 15 custom modules, ORM-based data access, background job processing | +| **Database** | PostgreSQL 16 | Odoo-managed schema, full-text search, JSONB for flexible data | +| **LMS** | OpenEduCat | Odoo-native education modules: courses, batches, attendance, timetable | +| **AI -- Text Generation** | OpenAI GPT-4o, GPT-3.5-turbo | Exam content generation, grading, evaluation, training tips | +| **AI -- Speech-to-Text** | OpenAI Whisper (local, base model) | 4 instances, round-robin, 30s chunked transcription | +| **AI -- Text-to-Speech** | AWS Polly (Neural) | 11 English voices, MP3 output, sentence-boundary chunking | +| **AI -- Video** | ELAI | 7 AI avatars (male/female), ElevenLabs + Azure voices | +| **AI -- Detection** | GPTZero | AI-generated writing detection per submission | +| **AI -- Search** | FAISS + all-MiniLM-L6-v2 | Semantic similarity search over training tips knowledge base | +| **Deployment** | Docker Compose | Odoo + PostgreSQL containers, Nginx reverse proxy | +| **Version Control** | Gitea | Private repositories for frontend and backend | + +### 3.3 Deployment Architecture + +```mermaid +graph LR + subgraph production ["Production Server"] + Nginx["Nginx
Reverse Proxy
SSL Termination"] + FrontendContainer["Frontend Container
React SPA
Port 3000"] + OdooContainer["Odoo 19 Container
Python 3.11
Port 8069"] + DBContainer["PostgreSQL 16
Port 5432"] + end + + Browser["Browser"] -->|"HTTPS"| Nginx + Nginx -->|"/app/*"| FrontendContainer + Nginx -->|"/api/*"| OdooContainer + OdooContainer --> DBContainer + FrontendContainer -->|"REST API"| OdooContainer +``` + +### 3.4 Existing Assets Inventory + +| Asset | Count | Status | +|-------|-------|--------| +| Odoo custom modules | 15 | Deployed on staging | +| Backend REST API endpoints | 70+ | Functional, English/IELTS focused | +| Frontend pages (React) | 55+ | UI complete, mock data only | +| AI service integrations | 6 | Configured and tested | +| ELAI avatar profiles | 7 | Seeded in database | +| Training tips (FAISS) | 80 | Imported with embeddings | +| Odoo data models | 20+ | Schema deployed | + +--- + +## 4. Feature Modules Breakdown + +### 4.1 English Self Learning + +| Aspect | Detail | +|--------|--------| +| **Current State** | Backend complete: AI-powered generation for Reading, Listening, Writing, Speaking. Grading via GPT-4o with IELTS rubric. Polly TTS for listening audio. ELAI avatars for speaking. FAISS-based personalized training tips. Frontend UI built (mocked). | +| **Work Required** | Replace mock authentication with Odoo JWT. Wire all student-facing pages to backend APIs. Integrate AI grading display (scores, feedback, AI detection). Connect training/tips module. | +| **Dependencies** | Stable Odoo API (Phase 1 complete). Frontend auth working. | +| **Effort** | 6 person-weeks | +| **Risk** | Low -- both backend and frontend exist; integration is the main task. | + +### 4.2 Examinations + +| Aspect | Detail | +|--------|--------| +| **Current State** | Backend complete: exam CRUD, 5 module types (reading, listening, writing, speaking, level), AI generation endpoints, approval workflows. Frontend: exam editor, exam list, generation page, approval workflows (all mocked). | +| **Work Required** | Wire admin exam management pages to APIs. Connect AI generation UI to real endpoints. Implement exam-taking flow with timer and state persistence. Connect grading pipeline and results display. | +| **Dependencies** | English Self Learning module (shared components). | +| **Effort** | Included in English Self Learning estimate (shared codebase). | +| **Risk** | Low -- core exam engine is proven. | + +### 4.3 Mathematics + +| Aspect | Detail | +|--------|--------| +| **Current State** | No backend module exists. No frontend pages exist. The AI exam engine (GPT-based generation + grading) provides a reusable foundation. | +| **Work Required** | **Backend:** Create `encoach_math` Odoo module with Math-specific exam models, AI prompt templates for formula-based questions, numerical grading logic, and multi-format question types (MCQ, fill-in, short answer, calculation). **Frontend:** Create Math course pages, Math exam taking interface with formula rendering (KaTeX/MathJax), Math-specific grading display. | +| **Dependencies** | Core exam engine must be stable. AI prompts require testing with GPT-4o for mathematical accuracy. | +| **Effort** | 5 person-weeks | +| **Risk** | Medium -- AI generation of math questions requires careful prompt engineering for accuracy. Formula rendering on frontend adds complexity. | + +### 4.4 Information Technology + +| Aspect | Detail | +|--------|--------| +| **Current State** | No backend module exists. No frontend pages exist. Similar to Math, the exam engine provides a foundation. | +| **Work Required** | **Backend:** Create `encoach_it` Odoo module with IT-specific exam models, AI prompt templates for conceptual and practical IT questions (networking, databases, programming concepts, security), grading logic for MCQ and short-answer. **Frontend:** Create IT course pages, IT exam interface, IT-specific grading display. | +| **Dependencies** | Core exam engine must be stable. | +| **Effort** | 4 person-weeks | +| **Risk** | Low-Medium -- IT subjects are text-based (no formula complexity). AI generation of IT questions is straightforward with GPT-4o. | + +### 4.5 LMS (OpenEduCat) + +| Aspect | Detail | +|--------|--------| +| **Current State** | OpenEduCat modules will be provided and installed into Odoo 19. Frontend LMS pages exist and are fully built: student dashboard, courses, course detail, assignments, grades, attendance, timetable (student); courses, course builder, assignments, attendance, students, timetable (teacher); dashboard, courses, students, teachers, batches, batch detail, timetable, reports, settings (admin). All pages use mock data. | +| **Work Required** | **Backend:** Install OpenEduCat modules. Create REST API controller layer (`encoach_lms_api`) to expose OpenEduCat models as JSON endpoints matching the frontend's data expectations. Map OpenEduCat models to frontend data interfaces (Course, Module, Lesson, Batch, Timetable, Attendance, Grade). **Frontend:** Replace mock data imports with API calls using TanStack Query. Wire forms to create/update APIs. | +| **Dependencies** | OpenEduCat modules provided and compatible with Odoo 19. | +| **Effort** | 3 person-weeks | +| **Risk** | Medium -- OpenEduCat model compatibility with Odoo 19 must be verified. API mapping between OpenEduCat's data structures and the frontend's interfaces requires careful alignment. | + +### 4.6 Whitelabeling + +| Aspect | Detail | +|--------|--------| +| **Current State** | No whitelabeling support. Frontend uses CSS variables for theming (Tailwind + shadcn/ui) with `darkMode: ["class"]` configured but not activated. No branding configuration. | +| **Work Required** | **Backend:** Create `encoach_branding` model in Odoo for tenant branding settings (logo, primary color, secondary color, font, favicon, login background, email header). Expose branding API endpoint. **Frontend:** Create a branding provider that fetches configuration on app load and applies CSS variables dynamically. Configure logo, colors, and fonts from API. Add admin UI for branding management. | +| **Dependencies** | UTAS provides brand assets (logo, color palette, fonts). | +| **Effort** | 2 person-weeks | +| **Risk** | Low -- CSS variable-based theming is well-established. | + +### 4.7 Humanisation (AI Virtual Tutors) + +| Aspect | Detail | +|--------|--------| +| **Current State** | ELAI integration exists for speaking exam avatar videos. 7 avatars configured (Gia, Vadim, Orhan, Flora, Scarlett, Parker, Ethan) with ElevenLabs and Azure voices. Currently limited to presenting speaking exam questions. | +| **Work Required** | **Backend:** Extend ELAI integration to support tutoring mode -- avatars explain concepts, provide feedback narration, and guide students through exercises. Create endpoints for on-demand tutoring video generation per subject. **Frontend:** Build tutor interaction UI -- avatar selector, video player with synchronized content, interactive Q&A flow. Integrate across English, Math, and IT modules. | +| **Dependencies** | ELAI API capacity and cost model for increased video generation. Subject-specific scripts/prompts. | +| **Effort** | 3 person-weeks | +| **Risk** | Medium -- ELAI video generation time (1-3 minutes per video) affects real-time tutoring UX. Cost scales with usage. | + +### 4.8 SIS Integration + +| Aspect | Detail | +|--------|--------| +| **Current State** | No SIS integration exists. UTAS has an existing Student Information System that we must integrate with via API. | +| **Work Required** | **Discovery:** Obtain UTAS SIS API documentation, authentication method, and data schemas. **Backend:** Create `encoach_sis` Odoo module with SIS sync models, scheduled sync jobs (cron), and conflict resolution. Implement: student enrollment import, grade export, attendance sync, course catalog mapping. **Frontend:** SIS sync status dashboard for admin. Sync logs and error reporting. | +| **Dependencies** | UTAS provides SIS API documentation, test environment access, and a technical contact. | +| **Effort** | 4 person-weeks | +| **Risk** | High -- entirely dependent on UTAS SIS API quality, documentation completeness, and test environment availability. This is the single highest-risk module. | + +### 4.9 Ticketing + +| Aspect | Detail | +|--------|--------| +| **Current State** | Backend complete: `encoach_ticket` Odoo module with full CRUD, categorization, and status workflow. API endpoints exist (`/api/tickets`). Frontend: Tickets page exists in admin portal (mocked). | +| **Work Required** | Wire frontend tickets page to backend API. Add ticket creation flow for students and teachers. Add ticket assignment and resolution workflow for admin. | +| **Dependencies** | None -- self-contained module. | +| **Effort** | 1 person-week | +| **Risk** | Low -- both backend and frontend exist. | + +### 4.10 Effort Summary + +| Module | Person-Weeks | Wave | +|--------|-------------|------| +| English Self Learning + Examinations | 6 | Wave 1 | +| Mathematics | 5 | Wave 2 | +| Information Technology | 4 | Wave 2 | +| LMS (OpenEduCat) | 3 | Wave 2 | +| Ticketing | 1 | Wave 2 | +| Whitelabeling | 2 | Wave 3 | +| Humanisation | 3 | Wave 3 | +| SIS Integration | 4 | Wave 3 | +| **Total** | **28 person-weeks** | | + +With a team of 3 producing approximately 3 person-weeks per calendar week, the raw work is approximately 9-10 calendar weeks. Adding UAT cycles, amendments, and buffer, the overall calendar duration is 20 weeks (April 1 -- August 21). + +--- + +## 5. Development Phases + +The project is structured into three overlapping waves. Each wave follows a build-test-deploy cycle and produces a production-ready deliverable. + +### 5.1 Wave 1 -- Foundation (April 1 -- June 12) + +**Goal:** English Self Learning and Examinations modules live in production with real UTAS users. + +This wave transforms the existing backend and frontend from separate prototypes into an integrated, production-ready platform. + +#### Sprint Breakdown + +| Sprint | Dates | Focus | Deliverables | +|--------|-------|-------|-------------| +| **S1** | Apr 1-11 | **Stabilization & Auth** | Fix remaining backend bugs. Replace mock auth with Odoo JWT in React frontend. Set up API service layer. Configure production deployment pipeline. | +| **S2** | Apr 12-18 | **Core API Integration** | Wire student dashboard, courses, and profile pages. Implement exam list and exam detail APIs. Connect user management and entity management. | +| **S3** | Apr 19-25 | **Exam Engine Integration** | Wire exam-taking flow (reading, listening, writing, speaking). Connect AI generation endpoints to exam editor. Integrate timer, state persistence, and submission. **Staging demo to UTAS (Apr 24-25).** | +| **S4** | Apr 26 -- May 9 | **AI Features & Grading** | Wire AI grading pipeline (writing evaluation, speaking evaluation). Connect TTS audio playback. Connect ELAI avatar videos. Integrate training tips and personalized recommendations. Connect AI detection display. | +| **S5** | May 10-23 | **UAT Round 1** | Deploy to UTAS staging. Conduct UAT with UTAS staff and selected students. Collect feedback. Bug fixes and amendments. | +| **S6** | May 24 -- Jun 6 | **UAT Round 2 & Hardening** | Address UAT feedback. Performance optimization. Security review. Documentation. | +| **S7** | Jun 7-12 | **GO LIVE** | Production deployment. Monitoring setup. Handover to UTAS for English + Examinations. Post-launch support begins. | + +#### Wave 1 Exit Criteria + +- Students can register, log in, and complete full English practice sessions +- AI grading returns accurate band scores with detailed feedback for writing and speaking +- Teachers can generate, review, approve, and assign exams +- Admin can manage users, entities, classrooms, and assignments +- Training tips are personalized based on exam performance +- Platform is stable under expected UTAS load + +### 5.2 Wave 2 -- Expansion (May 17 -- July 24) + +**Goal:** Mathematics, Information Technology, LMS, and Ticketing modules live in production. + +Wave 2 begins during Wave 1's UAT phase, leveraging developers freed from active feature work. + +#### Sprint Breakdown + +| Sprint | Dates | Focus | Deliverables | +|--------|-------|-------|-------------| +| **S8** | May 17-30 | **Math & IT Backend** | Create `encoach_math` and `encoach_it` Odoo modules. Develop AI prompt templates for Math question generation (numerical, formula-based, word problems). Develop AI prompt templates for IT questions (conceptual, practical). Implement grading logic for non-essay formats. | +| **S9** | May 31 -- Jun 13 | **OpenEduCat & LMS API** | Install and configure OpenEduCat modules in Odoo 19. Create `encoach_lms_api` controller layer. Map OpenEduCat models to frontend data interfaces. Wire LMS admin pages (courses, batches, timetable, attendance). | +| **S10** | Jun 14-27 | **Math & IT Frontend + LMS Wiring** | Build Math exam interface with formula rendering. Build IT exam interface. Wire student and teacher LMS pages. Wire ticketing module. | +| **S11** | Jun 28 -- Jul 11 | **UAT Round 1** | Deploy Math, IT, LMS, Ticketing to staging. UAT with UTAS staff. Math AI accuracy testing. LMS workflow validation. | +| **S12** | Jul 12-18 | **Amendments** | Address UAT feedback. Fix Math/IT grading edge cases. LMS refinements. | +| **S13** | Jul 19-24 | **GO LIVE** | Production deployment of Math, IT, LMS, Ticketing. Post-launch support begins. | + +#### Wave 2 Exit Criteria + +- Math and IT exams generate accurately via AI with appropriate difficulty levels +- LMS (OpenEduCat) fully functional: courses, enrollment, attendance, timetable, gradebook +- Ticketing system operational for students and staff +- All modules integrated with Wave 1 platform (single sign-on, unified navigation) + +### 5.3 Wave 3 -- Integration (July 1 -- August 21) + +**Goal:** Whitelabeling, SIS integration, and Humanisation modules complete. Full platform go-live. + +Wave 3 begins during Wave 2's frontend phase, with SIS discovery starting earlier. + +#### Sprint Breakdown + +| Sprint | Dates | Focus | Deliverables | +|--------|-------|-------|-------------| +| **S14** | Jul 1-11 | **Whitelabeling + SIS Discovery** | Implement branding configuration backend and frontend. Apply UTAS brand assets. Begin SIS API discovery and documentation review with UTAS IT team. | +| **S15** | Jul 12-25 | **SIS Integration Development** | Build `encoach_sis` Odoo module. Implement student enrollment import. Implement grade export. Implement attendance sync. Build admin SIS dashboard. | +| **S16** | Jul 26 -- Aug 1 | **Humanisation** | Extend ELAI integration for tutoring mode. Build tutor interaction UI. Generate subject-specific avatar tutoring content. | +| **S17** | Aug 2-8 | **Integration Testing** | End-to-end testing of all 9 modules together. SIS sync validation with UTAS test data. Performance testing under load. Security audit. | +| **S18** | Aug 9-15 | **Final UAT** | Full platform UAT with UTAS. All modules tested together. Final amendments. | +| **S19** | Aug 16-21 | **Full Platform GO LIVE** | Production deployment of complete platform. All 9 modules active. Monitoring and support in place. | + +#### Wave 3 Exit Criteria + +- Platform displays UTAS branding throughout +- SIS syncs student enrollment, grades, and attendance bidirectionally +- AI virtual tutors available across English, Math, and IT +- All 9 modules function as an integrated platform +- Performance meets production requirements under expected load + +--- + +## 6. University Integration Plan + +### 6.1 Integration Architecture + +```mermaid +sequenceDiagram + participant SIS as UTAS SIS + participant Odoo as EnCoach Odoo 19 + participant React as EnCoach Frontend + participant Student as Student + + Note over SIS,Odoo: Enrollment Sync (SIS → EnCoach) + SIS->>Odoo: Student enrollment data (scheduled sync) + Odoo->>Odoo: Create/update student records + Odoo->>Odoo: Assign to courses and batches + + Note over Student,React: Daily Usage + Student->>React: Login, take courses, complete exams + React->>Odoo: API calls (exam, grading, attendance) + Odoo->>Odoo: Record grades, attendance, progress + + Note over Odoo,SIS: Grade & Attendance Export (EnCoach → SIS) + Odoo->>SIS: Grade export (scheduled sync) + Odoo->>SIS: Attendance export (scheduled sync) +``` + +### 6.2 Data Exchange Specification + +| Data Flow | Direction | Frequency | Format | +|-----------|-----------|-----------|--------| +| Student enrollment | SIS → EnCoach | On enrollment + daily sync | REST API / CSV import | +| Course catalog mapping | Bidirectional | On setup + manual sync | REST API | +| Exam grades | EnCoach → SIS | After grading + daily batch | REST API | +| Attendance records | EnCoach → SIS | End of day batch | REST API | +| Student status changes | SIS → EnCoach | Real-time webhook or daily | REST API | + +### 6.3 Authentication Strategy + +| Phase | Method | Detail | +|-------|--------|--------| +| **Initial (Wave 1-2)** | Odoo-native JWT | Students and staff authenticate directly with EnCoach. Credentials provisioned from SIS enrollment data. | +| **Full Integration (Wave 3)** | SSO via SAML/OAuth | If UTAS SIS supports SSO, students authenticate once at UTAS portal and access EnCoach seamlessly. Fallback to JWT if SSO is not available. | + +### 6.4 UTAS Responsibilities for Integration + +| Deliverable | Required By | Impact if Delayed | +|-------------|-------------|-------------------| +| SIS API documentation | June 15, 2026 | Blocks SIS module development; delays Wave 3 by equivalent duration | +| SIS test environment access | July 1, 2026 | Blocks integration testing | +| SIS technical contact (dedicated) | June 15, 2026 | Slows discovery and issue resolution | +| Brand assets (logo, colors, fonts) | June 15, 2026 | Blocks whitelabeling completion | +| UAT testers (3-5 staff, 10-20 students) | May 1, 2026 | Delays UAT cycles; risks quality | +| Math and IT course content (if not AI-generated) | May 15, 2026 | Delays Math/IT module population | +| Production server provisioning | August 1, 2026 | Blocks production deployment | + +--- + +## 7. Master Timeline + +### 7.1 Gantt Chart + +```mermaid +gantt + title EnCoach UTAS Deployment -- Master Timeline + dateFormat YYYY-MM-DD + axisFormat %d %b + + section Wave1_Foundation + S1_Stabilize_Auth :s1, 2026-04-01, 11d + S2_Core_API_Integration :s2, 2026-04-12, 7d + S3_Exam_Engine_Integration :s3, 2026-04-19, 7d + UTAS_Staging_Demo :milestone, m1, 2026-04-24, 0d + S4_AI_Features_Grading :s4, 2026-04-26, 14d + S5_UAT_Round_1 :s5, 2026-05-10, 14d + S6_UAT_Round_2_Hardening :s6, 2026-05-24, 14d + S7_English_Exams_GO_LIVE :s7, 2026-06-07, 5d + English_Exams_LIVE :milestone, m2, 2026-06-12, 0d + + section Wave2_Expansion + S8_Math_IT_Backend :s8, 2026-05-17, 14d + S9_OpenEduCat_LMS_API :s9, 2026-05-31, 14d + S10_Math_IT_LMS_Frontend :s10, 2026-06-14, 14d + S11_UAT_Math_IT_LMS :s11, 2026-06-28, 14d + S12_Amendments :s12, 2026-07-12, 7d + S13_Math_IT_LMS_GO_LIVE :s13, 2026-07-19, 5d + Math_IT_LMS_LIVE :milestone, m3, 2026-07-24, 0d + + section Wave3_Integration + S14_Whitelabeling_SIS_Discovery :s14, 2026-07-01, 11d + S15_SIS_Integration :s15, 2026-07-12, 14d + S16_Humanisation :s16, 2026-07-26, 7d + S17_Integration_Testing :s17, 2026-08-02, 7d + S18_Final_UAT :s18, 2026-08-09, 7d + S19_Full_Platform_GO_LIVE :s19, 2026-08-16, 5d + Full_Platform_LIVE :milestone, m4, 2026-08-21, 0d + + section PostLaunch + Post_Launch_Support :pls, 2026-08-21, 30d +``` + +### 7.2 Key Milestones + +| # | Milestone | Date | Gate Criteria | +|---|-----------|------|---------------| +| M0 | Project Kickoff | April 1, 2026 | Team assembled, scope agreed, environments ready | +| M1 | UTAS Staging Demo | April 24, 2026 | English + Exams functional on staging, basic end-to-end flow | +| M2 | Wave 1 GO LIVE | June 12, 2026 | English Self Learning + Examinations in production, 2 UAT rounds passed | +| M3 | Wave 2 GO LIVE | July 24, 2026 | Math + IT + LMS + Ticketing in production, UAT passed | +| M4 | Full Platform GO LIVE | August 21, 2026 | All 9 modules in production, SIS integration validated, full UAT passed | +| M5 | Post-Launch Support Ends | September 21, 2026 | 30-day support period complete, handover documentation delivered | + +### 7.3 Critical Path + +The critical path runs through: + +1. **Wave 1 auth stabilization** (blocks all frontend work) +2. **Wave 1 API integration** (blocks UAT and all subsequent waves) +3. **UTAS SIS API documentation delivery** (blocks Wave 3 SIS development) +4. **OpenEduCat module compatibility** (blocks LMS development) + +Any delay on items 1-2 cascades through the entire timeline. Items 3-4 are external dependencies that must be tracked proactively. + +--- + +## 8. Testing and Rollout Strategy + +### 8.1 Testing Stages + +```mermaid +graph LR + Dev["Developer Testing
Unit + Integration
Continuous"] --> Staging["Staging
System Integration
Per Sprint"] + Staging --> UAT["UAT with UTAS
Acceptance
Per Wave"] + UAT --> Perf["Performance Testing
Load + Stress
Pre-Go-Live"] + Perf --> Security["Security Audit
OWASP Top 10
Pre-Go-Live"] + Security --> GoLive["GO LIVE
Production
Per Wave"] +``` + +| Stage | Who | When | Scope | +|-------|-----|------|-------| +| **Developer Testing** | EnCoach dev team | Continuous | Unit tests, API integration tests, manual smoke tests per feature | +| **Staging Deployment** | EnCoach dev team | End of each sprint | Full platform on staging server, regression testing, cross-module integration | +| **UAT (User Acceptance)** | UTAS staff + students | 2 rounds per wave | Guided test scenarios, real-world usage, feedback collection | +| **Performance Testing** | EnCoach dev team | Before each go-live | Concurrent user simulation (target: 200 simultaneous users), API response times (<2s for standard, <60s for AI grading) | +| **Security Audit** | EnCoach architect | Before production go-live | Authentication review, authorization checks, API input validation, OWASP Top 10 | + +### 8.2 UAT Process + +Each wave includes two UAT rounds: + +**UAT Round 1 (2 weeks):** +1. EnCoach deploys to staging environment +2. UTAS UAT team receives test accounts and test scenarios document +3. UTAS testers execute scenarios and log issues via shared tracking system +4. EnCoach triages issues daily (critical = same day, high = 2 days, medium = next sprint) +5. Weekly status call between EnCoach and UTAS + +**UAT Round 2 (2 weeks):** +1. All critical and high issues from Round 1 resolved +2. Re-test of failed scenarios +3. Exploratory testing by UTAS testers +4. Go-live readiness assessment +5. Sign-off by UTAS project lead + +### 8.3 Go-Live Checklist (Per Wave) + +| # | Item | Owner | +|---|------|-------| +| 1 | All critical and high UAT issues resolved | EnCoach | +| 2 | Performance tests pass (response time, concurrent users) | EnCoach | +| 3 | Security audit complete, no critical findings | EnCoach | +| 4 | Production environment provisioned and configured | EnCoach + UTAS IT | +| 5 | DNS / domain configured (if applicable) | UTAS IT | +| 6 | SSL certificate installed | EnCoach + UTAS IT | +| 7 | Database backup and restore procedure tested | EnCoach | +| 8 | Monitoring and alerting configured | EnCoach | +| 9 | Rollback procedure documented and tested | EnCoach | +| 10 | UTAS project lead sign-off | UTAS | + +### 8.4 Rollback Plan + +Each go-live deployment includes a documented rollback procedure: + +1. **Database snapshot** taken immediately before production deployment +2. **Previous container images** tagged and retained (minimum 3 previous versions) +3. **Rollback trigger criteria:** >5% error rate, >10s average response time, data integrity issue +4. **Rollback execution time:** <15 minutes (Docker image swap + database restore) +5. **Communication:** UTAS IT notified within 5 minutes of rollback decision + +### 8.5 Post-Launch Support + +| Period | Coverage | Scope | +|--------|----------|-------| +| Week 1-2 after each go-live | Daily monitoring, same-day critical fixes | Bug fixes, performance tuning, user support | +| Week 3-4 after each go-live | Standard monitoring, 2-day response for non-critical | Bug fixes, minor enhancements from feedback | +| After Full Platform GO LIVE | 30-day support period (Aug 21 -- Sep 21) | Full platform support, knowledge transfer to UTAS IT | + +--- + +## 9. Team and Responsibilities + +### 9.1 EnCoach Team + +| Role | Responsibilities | +|------|-----------------| +| **Solution Architect** | System architecture decisions. University integration design. SIS API mapping. Security and performance oversight. Code review. UTAS stakeholder communication. Sprint planning. | +| **Senior Developer (AI-assisted)** | Backend development (Odoo modules). AI service integration. Math/IT module AI prompts. SIS integration implementation. Database design. API development. | +| **Full-Stack Developer (AI-assisted)** | Frontend development (React). API integration. UI/UX implementation. Whitelabeling. LMS frontend wiring. Testing. | + +### 9.2 UTAS Responsibilities + +| Role | Responsibilities | +|------|-----------------| +| **Project Sponsor** | Budget approval. Strategic decisions. Escalation resolution. | +| **Project Lead** | Scope sign-off. UAT coordination. Go-live approval. Weekly status reviews. | +| **IT Department** | SIS API documentation and test environment. Production server provisioning. Network and DNS configuration. SSL certificates. | +| **Subject Matter Experts** | Math and IT course content review. Exam quality validation. Training content verification. | +| **UAT Testers (3-5 staff, 10-20 students)** | Execute test scenarios. Log issues and feedback. Re-test after fixes. | + +### 9.3 Communication Plan + +| Activity | Frequency | Participants | Medium | +|----------|-----------|-------------|--------| +| Sprint status report | Weekly (Friday) | EnCoach team → UTAS Project Lead | Email + document | +| Demo / walkthrough | Bi-weekly | EnCoach team + UTAS stakeholders | Video call + screen share | +| UAT feedback review | Daily during UAT | EnCoach team + UTAS testers | Shared issue tracker | +| Escalation call | As needed | Architect + UTAS Project Lead | Video call | +| Go-live readiness review | Before each go-live | All stakeholders | Meeting | + +### 9.4 Escalation Path + +``` +Level 1: Developer → Solution Architect (same day) +Level 2: Solution Architect → UTAS Project Lead (1 business day) +Level 3: UTAS Project Lead → UTAS Project Sponsor (2 business days) +``` + +--- + +## 10. Risk Register + +| # | Risk | Probability | Impact | Mitigation | +|---|------|------------|--------|------------| +| R1 | **UTAS SIS API documentation delayed or incomplete** | High | High | Begin SIS discovery in Wave 2 (early July). Prepare alternative sync methods (CSV import/export) as fallback. Weekly check-ins with UTAS IT from June onwards. | +| R2 | **Math AI question generation inaccuracy** | Medium | Medium | Invest in thorough prompt engineering with GPT-4o. Build validation test suite with known-correct Math problems. Include human review step in exam approval workflow. | +| R3 | **OpenEduCat incompatibility with Odoo 19** | Medium | High | Test OpenEduCat installation early (Sprint S9). Identify compatibility issues before committing to LMS frontend work. Fallback: build minimal LMS models within existing Odoo modules. | +| R4 | **Frontend API integration slower than estimated** | Medium | Medium | Prioritize critical paths (auth, core pages) first. Use API contract testing to parallelize frontend and backend work. Leverage AI coding assistants to accelerate repetitive wiring. | +| R5 | **Team bandwidth constraints** | Medium | High | AI-assisted development (both developers use AI tools) effectively increases output by 1.5-2x. Strict scope management -- no feature creep. Clear sprint goals with daily standups. | +| R6 | **ELAI video generation latency for Humanisation** | Low | Medium | Pre-generate common tutoring videos. Cache frequently requested content. Set user expectations (1-3 min generation time). Consider alternative: pre-recorded avatar videos for standard content. | +| R7 | **Production infrastructure not ready on time** | Low | High | Request production server provisioning by August 1 (3 weeks before go-live). Test deployment procedure on staging first. Have cloud-based fallback plan. | +| R8 | **UTAS UAT testers unavailable or insufficient feedback** | Medium | Medium | Provide detailed test scenarios with step-by-step instructions. Offer guided UAT sessions. Establish minimum UAT participation requirements in the scope agreement. | + +--- + +## 11. Assumptions and Prerequisites + +### 11.1 Assumptions + +| # | Assumption | Impact if Invalid | +|---|-----------|-------------------| +| A1 | UTAS provides 3-5 staff and 10-20 students for UAT testing | Delays UAT; reduces quality assurance | +| A2 | OpenEduCat modules are compatible with Odoo 19 | Requires building custom LMS models; adds 3-4 weeks | +| A3 | UTAS SIS exposes REST APIs for enrollment, grades, and attendance | Requires alternative integration (CSV, database sync); adds 2-3 weeks | +| A4 | Math and IT course content is either provided by UTAS or generated by AI | If neither, course content creation becomes a separate workstream | +| A5 | The platform will be accessed via web browsers only (no mobile app) | Mobile app would be a separate project | +| A6 | Institutional licensing model (no per-student payment processing) | Payment gateway integration would add 2-3 weeks | +| A7 | Single UTAS deployment (not multi-university / multi-tenant initially) | Multi-tenant support would add architectural complexity | +| A8 | Internet connectivity at UTAS is sufficient for AI cloud services (OpenAI, ELAI, AWS Polly) | On-premise AI deployment would be a separate project | + +### 11.2 Prerequisites (with Deadlines) + +| # | Prerequisite | Owner | Deadline | Status | +|---|-------------|-------|----------|--------| +| P1 | Scope agreement signed | UTAS + EnCoach | March 31, 2026 | Pending | +| P2 | OpenEduCat modules provided | EnCoach / UTAS | May 15, 2026 | Pending | +| P3 | UTAS brand assets delivered | UTAS | June 15, 2026 | Pending | +| P4 | SIS API documentation provided | UTAS IT | June 15, 2026 | Pending | +| P5 | SIS test environment access granted | UTAS IT | July 1, 2026 | Pending | +| P6 | UAT test team identified | UTAS | May 1, 2026 | Pending | +| P7 | Production server provisioned | UTAS IT / EnCoach | August 1, 2026 | Pending | +| P8 | Math/IT course content available | UTAS SMEs | May 15, 2026 | Pending | + +--- + +## Appendix A: Module-to-API Mapping (Existing Backend Endpoints) + +The following 70+ API endpoints are already built and deployed in the Odoo 19 backend. These form the foundation for Wave 1 frontend integration. + +| Category | Endpoints | Count | +|----------|-----------|-------| +| Authentication | `/api/login`, `/api/logout`, `/api/reset`, `/api/reset/sendVerification` | 4 | +| Users | `/api/user`, `/api/users/update`, `/api/users/list`, `/api/users/` | 4 | +| Entities & Roles | `/api/entities`, `/api/roles`, `/api/permissions` | 7 | +| Exams | `/api/exam`, `/api/exam/`, `/api/exam//`, `/api/exam/avatars` | 7 | +| AI Generation | `/api/exam/reading/*`, `/api/exam/listening/*`, `/api/exam/writing/*`, `/api/exam/speaking/*`, `/api/exam/level/generate` | 8 | +| Media | `/api/exam/listening/media`, `/api/exam/listening/transcribe`, `/api/exam/speaking/media`, `/api/transcribe` | 6 | +| Evaluations & Grading | `/api/evaluate/writing`, `/api/evaluate/speaking`, `/api/evaluate/interactiveSpeaking`, `/api/grading/multiple` | 6 | +| Classrooms | `/api/groups`, `/api/groups/` | 3 | +| Assignments | `/api/assignments`, `/api/assignments/`, `/api/assignments//start`, release, archive | 8 | +| Sessions & Stats | `/api/sessions`, `/api/stats`, `/api/statistical` | 5 | +| Training | `/api/training`, `/api/training/`, `/api/training/tips`, `/api/training/walkthrough` | 4 | +| Subscriptions | `/api/packages`, `/api/stripe`, `/api/paypal`, `/api/paymob` + webhooks | 7 | +| Registration | `/api/register`, `/api/code/`, `/api/batch_users`, `/api/make_user` | 4 | +| Tickets | `/api/tickets`, `/api/tickets/` | 3 | +| Storage | `/api/storage`, `/api/storage/delete` | 2 | +| Approvals | `/api/approval-workflows`, `/api/approval-workflows/` | 3 | +| **Total** | | **~81** | + +--- + +## Appendix B: AI Service Configuration Summary + +| Service | Model | Key Config | Usage | +|---------|-------|-----------|-------| +| OpenAI GPT | gpt-4o (primary), gpt-3.5-turbo (secondary) | Temperature: 0.1 (grading), 0.7 (generation), 0.2 (summaries). Max tokens: 4,097. JSON response format. | Exam generation, grading, evaluation, training tips | +| Whisper | base (local, ~1GB) | 4 instances, round-robin, 16kHz mono, 30s chunks with overlap | Speaking transcription, audio-to-text | +| AWS Polly | Neural engine | 11 voices, MP3 output, 3,000 char chunks | Listening exam audio generation | +| ELAI | 7 avatars | ElevenLabs + Azure voices, fade_in animation | Speaking exam video, virtual tutoring | +| GPTZero | v2/predict/text | Per-sentence analysis, confidence scoring | Writing AI detection | +| FAISS | IndexFlatL2 + all-MiniLM-L6-v2 | Top-5 results, 7 category indices, 80 training tips | Personalized training recommendations | + +--- + +## Appendix C: Frontend Page Inventory (New React Application) + +| Portal | Pages | Key Features | +|--------|-------|-------------| +| **Student** | Dashboard, Courses, Course Detail, Assignments, Grades, Attendance, Timetable, Profile | 8 pages | +| **Teacher** | Dashboard, Courses, Course Builder, Course Edit, Assignments, Assignment Detail, Attendance, Students, Timetable, Profile | 10 pages | +| **Admin (LMS)** | Dashboard, Courses, Students, Teachers, Batches, Batch Detail, Timetable, Reports, Settings, Profile | 10 pages | +| **Admin (Platform)** | Platform Dashboard, Assignments, Exams List, Exam Structures, Rubrics, Generation, Approval Workflows, Users, Entities, Classrooms, Student Performance, Stats Corporate, Record, Vocabulary, Grammar, Payment Record, Tickets, Settings, Exam | 19 pages | +| **Auth** | Login, Register, Forgot Password | 3 pages | +| **Total** | | **~50+ pages** | + +--- + +*Document prepared by EnCoach Engineering Team. For questions or clarifications, contact the Solution Architect.* diff --git a/docs/UTAS_SYSTEM_SCOPE.md b/docs/UTAS_SYSTEM_SCOPE.md new file mode 100644 index 00000000..30f6189d --- /dev/null +++ b/docs/UTAS_SYSTEM_SCOPE.md @@ -0,0 +1,86 @@ +# EnCoach -- UTAS System Scope Definition + +**Date:** March 16, 2026 +**Project:** EnCoach University Deployment for UTAS + +--- + +## Core Modules + +### 1. Learning Management System (LMS) + +| Item | Detail | +|------|--------| +| **Engine** | OpenEduCat (Odoo 19 native modules) | +| **Frontend** | React 18 SPA -- student, teacher, and admin portals (50+ pages built) | +| **Capabilities** | Course catalog and enrollment, batch/cohort management, timetable scheduling, attendance tracking, gradebook, academic reporting | +| **Integration** | REST API bridge between OpenEduCat models and React frontend | +| **Subjects** | English Self Learning, Mathematics, Information Technology (extensible) | + +### 2. Exam Portal + +| Item | Detail | +|------|--------| +| **Exam Types** | Reading, Listening, Writing, Speaking, Level Test, Math, IT | +| **AI Generation** | GPT-4o generates exam content per subject and difficulty level | +| **AI Grading** | Automated grading for writing (GPT-4o + IELTS rubric), speaking (Whisper STT + GPT-4o), MCQ, short answer, numerical | +| **AI Detection** | GPTZero flags AI-generated student submissions | +| **Media** | AWS Polly TTS for listening audio, ELAI avatars for speaking video | +| **Workflow** | Exam creation, review, approval, assignment, timed sessions, grading, results | +| **Roles** | Admin creates/approves, teacher assigns, student takes, system grades | + +### 3. Student Onboarding + +| Item | Detail | +|------|--------| +| **Registration** | Self-registration or bulk import (CSV/batch) by admin | +| **SIS Sync** | Automatic student provisioning from UTAS Student Information System | +| **Enrollment** | Assignment to courses, batches, and classrooms upon registration | +| **Verification** | Email verification, registration code validation | +| **Roles** | Student, teacher, admin -- role-based access from first login | + +### 4. Knowledge Base + +| Item | Detail | +|------|--------| +| **Training Tips** | 80+ categorized tips (reading, writing, strategy, vocabulary, grammar) with FAISS semantic search | +| **Personalization** | After each exam, GPT analyzes performance and retrieves relevant tips via RAG (Retrieval-Augmented Generation) | +| **Walkthroughs** | Per-module guided learning paths | +| **AI Tutoring** | ELAI avatar virtual tutors provide video-based explanations and guidance (Humanisation) | +| **Content Scope** | English/IELTS (existing), Math and IT (to be developed) | + +### 5. Helpdesk System + +| Item | Detail | +|------|--------| +| **Ticketing** | Students and staff create support tickets with category, priority, and description | +| **Workflow** | Creation, assignment, status tracking (open, in-progress, resolved, closed) | +| **Roles** | Students/teachers submit; admin staff categorize, assign, and resolve | +| **Backend** | `encoach_ticket` Odoo module with full CRUD API (`/api/tickets`) | + +### 6. API Integration with SIS + +| Item | Detail | +|------|--------| +| **Direction** | Bidirectional: UTAS SIS <--> EnCoach | +| **Inbound (SIS to EnCoach)** | Student enrollment data, course catalog mapping, student status changes | +| **Outbound (EnCoach to SIS)** | Exam grades, attendance records, course completion status | +| **Method** | REST API (preferred) or CSV import/export (fallback) | +| **Frequency** | Real-time webhook (if available) or scheduled sync (daily batch) | +| **Prerequisite** | UTAS provides SIS API documentation, test environment, and technical contact | + +--- + +## Technology Stack Summary + +| Layer | Technology | +|-------|-----------| +| Frontend | React 18, TypeScript, Vite, Tailwind CSS, shadcn/ui | +| Backend | Odoo 19, Python 3.11, PostgreSQL 16 | +| LMS | OpenEduCat (Odoo modules) | +| AI | OpenAI GPT-4o, Whisper, AWS Polly, ELAI, GPTZero, FAISS | +| Deployment | Docker Compose, Nginx | + +--- + +*EnCoach Engineering -- UTAS Deployment* diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..40f72cc4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,26 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "@typescript-eslint/no-unused-vars": "off", + }, + }, +); diff --git a/index.html b/index.html new file mode 100644 index 00000000..c54f1021 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + + EnCoach — IELTS & English Learning Platform + + + + + + + + + + +

+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f5e02efc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8333 @@ +{ + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.8.tgz", + "integrity": "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz", + "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.24", + "@swc/core-darwin-x64": "1.15.24", + "@swc/core-linux-arm-gnueabihf": "1.15.24", + "@swc/core-linux-arm64-gnu": "1.15.24", + "@swc/core-linux-arm64-musl": "1.15.24", + "@swc/core-linux-ppc64-gnu": "1.15.24", + "@swc/core-linux-s390x-gnu": "1.15.24", + "@swc/core-linux-x64-gnu": "1.15.24", + "@swc/core-linux-x64-musl": "1.15.24", + "@swc/core-win32-arm64-msvc": "1.15.24", + "@swc/core-win32-ia32-msvc": "1.15.24", + "@swc/core-win32-x64-msvc": "1.15.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz", + "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz", + "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz", + "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz", + "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz", + "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz", + "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz", + "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz", + "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz", + "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz", + "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz", + "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz", + "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.97.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.97.0.tgz", + "integrity": "sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.97.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.97.0.tgz", + "integrity": "sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.97.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lovable-tagger": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/lovable-tagger/-/lovable-tagger-1.1.13.tgz", + "integrity": "sha512-RBEYDxao7Xf8ya29L0cd+ocE7Gs80xPOIOwwck65Hoie8YDKViuXi3UYV14DoNWIvaJ7WVPf7SG3cc844nFqGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "tailwindcss": "^3.4.17" + }, + "peerDependencies": { + "vite": ">=5.0.0 <8.0.0" + } + }, + "node_modules/lucide-react": { + "version": "0.462.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", + "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.72.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", + "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", + "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.1", + "@typescript-eslint/parser": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vaul": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", + "integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..04b23658 --- /dev/null +++ b/package.json @@ -0,0 +1,89 @@ +{ + "name": "vite_react_shadcn_ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build --mode development", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@tailwindcss/typography": "^0.5.16", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react-swc": "^3.11.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^5.4.19", + "vitest": "^3.2.4" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3c01d69713f9c184e92b74f5799e6dff2f500825 GIT binary patch literal 20373 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 literal 0 HcmV?d00001 diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fc5e9d9051e719ef5da398b6cab50ae0217af026 GIT binary patch literal 49453 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 GJ^eqn(uAP^ literal 0 HcmV?d00001 diff --git a/public/placeholder.svg b/public/placeholder.svg new file mode 100644 index 00000000..e763910b --- /dev/null +++ b/public/placeholder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..6018e701 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,14 @@ +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +User-agent: Twitterbot +Allow: / + +User-agent: facebookexternalhit +Allow: / + +User-agent: * +Allow: / diff --git a/public/templates/students.csv b/public/templates/students.csv new file mode 100644 index 00000000..d5a129ae --- /dev/null +++ b/public/templates/students.csv @@ -0,0 +1,2 @@ +name,email +Jane Doe,jane@example.com diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..0d220518 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,345 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route, Navigate, useNavigate } from "react-router-dom"; +import { AuthProvider } from "@/contexts/AuthContext"; +import ProtectedRoute from "@/components/ProtectedRoute"; +import StudentLayout from "@/components/StudentLayout"; +import TeacherLayout from "@/components/TeacherLayout"; +import AdminLmsLayout from "@/components/AdminLmsLayout"; +import Login from "@/pages/Login"; +import Register from "@/pages/Register"; +import EmailVerification from "@/pages/EmailVerification"; +import OnboardingWizard from "@/pages/OnboardingWizard"; +import ForgotPassword from "@/pages/ForgotPassword"; +import ResetPassword from "@/pages/ResetPassword"; +import ScoreVerification from "@/pages/ScoreVerification"; +// Original platform pages +import AdminDashboard from "@/pages/AdminDashboard"; +import UsersPage from "@/pages/UsersPage"; +import EntitiesPage from "@/pages/EntitiesPage"; +import AssignmentsPage from "@/pages/AssignmentsPage"; +import ExamsListPage from "@/pages/ExamsListPage"; +import ExamStructuresPage from "@/pages/ExamStructuresPage"; +import RubricsPage from "@/pages/RubricsPage"; +import GenerationPage from "@/pages/GenerationPage"; +import ApprovalWorkflowsPage from "@/pages/ApprovalWorkflowsPage"; +import ClassroomsPage from "@/pages/ClassroomsPage"; +import StudentPerformancePage from "@/pages/StudentPerformancePage"; +import StatsCorporatePage from "@/pages/StatsCorporatePage"; +import RecordPage from "@/pages/RecordPage"; +import VocabularyPage from "@/pages/VocabularyPage"; +import GrammarPage from "@/pages/GrammarPage"; +import PaymentRecordPage from "@/pages/PaymentRecordPage"; +import TicketsPage from "@/pages/TicketsPage"; +import SettingsPage from "@/pages/SettingsPage"; +import ProfilePage from "@/pages/ProfilePage"; +import ExamPage from "@/pages/ExamPage"; +// Student pages +import StudentDashboard from "@/pages/student/StudentDashboard"; +import StudentCourses from "@/pages/student/StudentCourses"; +import StudentCourseDetail from "@/pages/student/StudentCourseDetail"; +import StudentAssignments from "@/pages/student/StudentAssignments"; +import StudentGrades from "@/pages/student/StudentGrades"; +import StudentAttendance from "@/pages/student/StudentAttendance"; +import StudentTimetable from "@/pages/student/StudentTimetable"; +import StudentProfile from "@/pages/student/StudentProfile"; +// Adaptive learning pages +import SubjectSelection from "@/pages/student/SubjectSelection"; +import DiagnosticTest from "@/pages/student/DiagnosticTest"; +import ProficiencyProfile from "@/pages/student/ProficiencyProfile"; +import LearningPlanPage from "@/pages/student/LearningPlan"; +import TopicLearning from "@/pages/student/TopicLearning"; +// Teacher pages +import TeacherDashboard from "@/pages/teacher/TeacherDashboard"; +import TeacherCourses from "@/pages/teacher/TeacherCourses"; +import CourseBuilder from "@/pages/teacher/CourseBuilder"; +import TeacherAssignments from "@/pages/teacher/TeacherAssignments"; +import TeacherAssignmentDetail from "@/pages/teacher/TeacherAssignmentDetail"; +import TeacherAttendance from "@/pages/teacher/TeacherAttendance"; +import TeacherStudents from "@/pages/teacher/TeacherStudents"; +import TeacherTimetable from "@/pages/teacher/TeacherTimetable"; +import TeacherProfile from "@/pages/teacher/TeacherProfile"; +import AdaptiveSettings from "@/pages/teacher/AdaptiveSettings"; +// Admin LMS pages +import AdminLmsDashboard from "@/pages/admin/AdminLmsDashboard"; +import AdminCourses from "@/pages/admin/AdminCourses"; +import AdminStudents from "@/pages/admin/AdminStudents"; +import AdminTeachers from "@/pages/admin/AdminTeachers"; +import AdminBatches from "@/pages/admin/AdminBatches"; +import AdminBatchDetail from "@/pages/admin/AdminBatchDetail"; +import AdminTimetable from "@/pages/admin/AdminTimetable"; +import AdminReports from "@/pages/admin/AdminReports"; +import AdminSettings from "@/pages/admin/AdminSettings"; +import AdminProfileLms from "@/pages/admin/AdminProfile"; +import TaxonomyManager from "@/pages/admin/TaxonomyManager"; +import ResourceManager from "@/pages/admin/ResourceManager"; +import AcademicYearManager from "@/pages/admin/AcademicYearManager"; +import DepartmentManager from "@/pages/admin/DepartmentManager"; +import AdmissionList from "@/pages/admin/AdmissionList"; +import AdmissionDetail from "@/pages/admin/AdmissionDetail"; +import AdmissionRegisterPage from "@/pages/admin/AdmissionRegisterPage"; +import InstitutionalExamSessions from "@/pages/admin/InstitutionalExamSessions"; +import MarksheetManager from "@/pages/admin/MarksheetManager"; +import AdminStudentLeave from "@/pages/admin/AdminStudentLeave"; +import AdminFees from "@/pages/admin/AdminFees"; +import AdminLessons from "@/pages/admin/AdminLessons"; +import AdminGradebook from "@/pages/admin/AdminGradebook"; +import AdminStudentProgress from "@/pages/admin/AdminStudentProgress"; +import AdminLibrary from "@/pages/admin/AdminLibrary"; +import AdminActivities from "@/pages/admin/AdminActivities"; +import AdminFacilities from "@/pages/admin/AdminFacilities"; +import AdmissionApplication from "@/pages/AdmissionApplication"; +import SubjectRegistrationPage from "@/pages/student/SubjectRegistrationPage"; +// Courseware pages +import CourseChapters from "@/pages/teacher/CourseChapters"; +import ChapterDetail from "@/pages/teacher/ChapterDetail"; +import AiWorkbench from "@/pages/teacher/AiWorkbench"; +import TeacherDiscussionBoard from "@/pages/teacher/TeacherDiscussionBoard"; +import TeacherAnnouncements from "@/pages/teacher/TeacherAnnouncements"; +import StudentChapterView from "@/pages/student/StudentChapterView"; +import StudentDiscussionBoard from "@/pages/student/StudentDiscussionBoard"; +import StudentAnnouncements from "@/pages/student/StudentAnnouncements"; +import StudentMessages from "@/pages/student/StudentMessages"; +import StudentJourney from "@/pages/student/StudentJourney"; +import AiEnglishCourse from "@/pages/student/AiEnglishCourse"; +import AiIeltsCourse from "@/pages/student/AiIeltsCourse"; +import ExamSession from "@/pages/student/ExamSession"; +import ExamStatus from "@/pages/student/ExamStatus"; +import ExamResults from "@/pages/student/ExamResults"; +import GapAnalysis from "@/pages/student/GapAnalysis"; +import CourseDelivery from "@/pages/student/CourseDelivery"; +import GradingQueue from "@/pages/admin/GradingQueue"; +import CourseConfig from "@/pages/admin/CourseConfig"; +import ModuleBuilder from "@/pages/admin/ModuleBuilder"; +import CourseProgress from "@/pages/teacher/CourseProgress"; +import PlacementBriefing from "@/pages/student/PlacementBriefing"; +import PlacementTest from "@/pages/student/PlacementTest"; +import PlacementResults from "@/pages/student/PlacementResults"; +import PlacementAccess from "@/pages/student/PlacementAccess"; +import FaqManager from "@/pages/admin/FaqManager"; +import NotificationRules from "@/pages/admin/NotificationRules"; +import ApprovalWorkflowConfig from "@/pages/admin/ApprovalWorkflowConfig"; +import RolesPermissions from "@/pages/admin/RolesPermissions"; +import AuthorityMatrix from "@/pages/admin/AuthorityMatrix"; +import UserRoles from "@/pages/admin/UserRoles"; +import BulkStudentUpload from "@/pages/admin/BulkStudentUpload"; +import CredentialDashboard from "@/pages/admin/CredentialDashboard"; +import AiEnglishQuality from "@/pages/admin/AiEnglishQuality"; +import AiEnglishTaxonomy from "@/pages/admin/AiEnglishTaxonomy"; +import AiIeltsValidation from "@/pages/admin/AiIeltsValidation"; +import AdaptiveDashboard from "@/pages/admin/AdaptiveDashboard"; +import AdaptiveStudentDetail from "@/pages/admin/AdaptiveStudentDetail"; +import LevelMappingConfig from "@/pages/admin/LevelMappingConfig"; +import WhiteLabelBranding from "@/pages/admin/WhiteLabelBranding"; +import ScoreApprovalQueue from "@/pages/admin/ScoreApprovalQueue"; +import ExamTemplateSelection from "@/pages/admin/ExamTemplateSelection"; +import IeltsExamCreate from "@/pages/admin/IeltsExamCreate"; +import IeltsSkillConfig from "@/pages/admin/IeltsSkillConfig"; +import IeltsContentPool from "@/pages/admin/IeltsContentPool"; +import IeltsExamValidation from "@/pages/admin/IeltsExamValidation"; +import CustomExamCreate from "@/pages/admin/CustomExamCreate"; +import OfficialExamAccess from "@/pages/OfficialExamAccess"; +import FaqPage from "@/pages/FaqPage"; +import NotFound from "@/pages/NotFound"; +import { queryClient } from "@/lib/query-client"; +import { Button } from "@/components/ui/button"; + +function StudentSubscriptionPlaceholder() { + const navigate = useNavigate(); + return ( +
+

Subscription checkout will be available here.

+ +
+ ); +} + +const App = () => ( + + + + + + + + {/* Auth routes */} + } /> + } /> + } /> + } /> + } /> + } /> + {/* Public pages */} + } /> + } /> + } /> + } /> + } /> + + }> + } /> + + + {/* Student routes */} + }> + } /> + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + {/* Teacher routes */} + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + {/* Admin routes — unified: original platform + LMS + AI */} + }> + }> + {/* LMS Dashboard */} + } /> + {/* Original platform dashboard */} + } /> + {/* LMS pages */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Original academic pages */} + } /> + } /> + } /> + } /> + } /> + } /> + {/* Original management pages */} + } /> + } /> + } /> + {/* Original report pages */} + } /> + } /> + } /> + {/* Training */} + } /> + } /> + {/* Support */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Institutional LMS pages */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* OpenEduCat module pages */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + {/* Redirects */} + } /> + } /> + {/* Legacy route redirects */} + } /> + } /> + + + + + +); + +export default App; diff --git a/src/components/AdminLmsLayout.tsx b/src/components/AdminLmsLayout.tsx new file mode 100644 index 00000000..34594d08 --- /dev/null +++ b/src/components/AdminLmsLayout.tsx @@ -0,0 +1,354 @@ +import { Outlet, Link, useNavigate, useLocation } from "react-router-dom"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { + Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, + SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarHeader, SidebarFooter, SidebarSeparator, useSidebar, +} from "@/components/ui/sidebar"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { NavLink } from "@/components/NavLink"; +import { useAuth } from "@/contexts/AuthContext"; +import NotificationDropdown from "@/components/NotificationDropdown"; +import AiAssistantDrawer from "@/components/ai/AiAssistantDrawer"; +import AiSearchBar from "@/components/ai/AiSearchBar"; +import { usePermissions } from "@/hooks/usePermissions"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, + BreadcrumbPage, BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { + GraduationCap, LogOut, User, Settings, LayoutDashboard, ClipboardList, + FileText, Layers, BookOpen, Wand2, GitBranch, Users, BarChart3, + Building2, History, CreditCard, Ticket, BookA, PenTool, + Calendar, ChevronDown, HelpCircle, LucideIcon, Target, FolderOpen, + CalendarDays, Landmark, UserPlus, ScrollText, Award, + HelpCircle as FaqIcon, Bell, Workflow, + CalendarOff, DollarSign, BookMarked, BarChartHorizontal, TrendingUp, + Library, Activity, Warehouse, UserCog, +} from "lucide-react"; +import React from "react"; + +// ============= Navigation Config ============= +interface NavItem { title: string; url: string; icon: LucideIcon } + +const overviewItems: NavItem[] = [ + { title: "Admin Dashboard", url: "/admin/dashboard", icon: LayoutDashboard }, + { title: "Platform Dashboard", url: "/admin/platform", icon: BarChart3 }, +]; + +const lmsItems: NavItem[] = [ + { title: "Courses", url: "/admin/courses", icon: BookOpen }, + { title: "Students", url: "/admin/students", icon: Users }, + { title: "Teachers", url: "/admin/teachers", icon: GraduationCap }, + { title: "Batches", url: "/admin/batches", icon: Layers }, + { title: "Timetable", url: "/admin/timetable", icon: Calendar }, + { title: "Reports", url: "/admin/reports", icon: BarChart3 }, +]; + +const academicItems: NavItem[] = [ + { title: "Assignments", url: "/admin/assignments", icon: ClipboardList }, + { title: "Exams List", url: "/admin/examsList", icon: FileText }, + { title: "Exam Structures", url: "/admin/exam-structures", icon: Layers }, + { title: "Rubrics", url: "/admin/rubrics", icon: BookOpen }, + { title: "Generation", url: "/admin/generation", icon: Wand2 }, + { title: "Approval Workflows", url: "/admin/approval-workflows", icon: GitBranch }, +]; + +const adaptiveItems: NavItem[] = [ + { title: "Taxonomy", url: "/admin/taxonomy", icon: Target }, + { title: "Resources", url: "/admin/resources", icon: FolderOpen }, +]; + +const institutionalItems: NavItem[] = [ + { title: "Academic Years", url: "/admin/academic-years", icon: CalendarDays }, + { title: "Departments", url: "/admin/departments", icon: Landmark }, + { title: "Admissions", url: "/admin/admissions", icon: UserPlus }, + { title: "Admission Register", url: "/admin/admission-register", icon: ScrollText }, + { title: "Exam Sessions", url: "/admin/exam-sessions", icon: FileText }, + { title: "Marksheets", url: "/admin/marksheets", icon: Award }, + { title: "Student Leave", url: "/admin/student-leave", icon: CalendarOff }, + { title: "Fees & Payments", url: "/admin/fees", icon: DollarSign }, + { title: "Lessons", url: "/admin/lessons", icon: BookMarked }, + { title: "Gradebook", url: "/admin/gradebook", icon: BarChartHorizontal }, + { title: "Student Progress", url: "/admin/student-progress", icon: TrendingUp }, + { title: "Library", url: "/admin/library", icon: Library }, + { title: "Activities", url: "/admin/activities", icon: Activity }, + { title: "Facilities", url: "/admin/facilities", icon: Warehouse }, +]; + +const managementItems: NavItem[] = [ + { title: "Users", url: "/admin/users", icon: Users }, + { title: "Entities", url: "/admin/entities", icon: Building2 }, + { title: "Classrooms", url: "/admin/classrooms", icon: GraduationCap }, + { title: "User Roles", url: "/admin/user-roles", icon: UserCog }, + { title: "Roles & Permissions", url: "/admin/roles-permissions", icon: Settings }, + { title: "Authority Matrix", url: "/admin/authority-matrix", icon: Workflow }, +]; + +const reportItems: NavItem[] = [ + { title: "Student Performance", url: "/admin/student-performance", icon: BarChart3 }, + { title: "Stats Corporate", url: "/admin/stats-corporate", icon: Building2 }, + { title: "Record", url: "/admin/record", icon: History }, +]; + +const trainingItems: NavItem[] = [ + { title: "Vocabulary", url: "/admin/training/vocabulary", icon: BookA }, + { title: "Grammar", url: "/admin/training/grammar", icon: PenTool }, +]; + +const configItems: NavItem[] = [ + { title: "FAQ Manager", url: "/admin/faq", icon: FaqIcon }, + { title: "Notification Rules", url: "/admin/notification-rules", icon: Bell }, + { title: "Approval Config", url: "/admin/approval-config", icon: Workflow }, +]; + +const supportItems: NavItem[] = [ + { title: "Payment Record", url: "/admin/payment-record", icon: CreditCard }, + { title: "Tickets", url: "/admin/tickets", icon: Ticket }, + { title: "Settings", url: "/admin/settings-platform", icon: Settings }, +]; + +// ============= Reusable sidebar group ============= +function SidebarNavGroup({ label, items }: { label: string; items: NavItem[] }) { + const { state } = useSidebar(); + const collapsed = state === "collapsed"; + + return ( + + {label} + + + {items.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + ); +} + +// ============= Breadcrumbs ============= +const routeLabels: Record = { + admin: "Admin", dashboard: "Dashboard", platform: "Platform", courses: "Courses", + students: "Students", teachers: "Teachers", batches: "Batches", timetable: "Timetable", + reports: "Reports", assignments: "Assignments", examsList: "Exams List", + "exam-structures": "Exam Structures", rubrics: "Rubrics", generation: "Generation", + "approval-workflows": "Approval Workflows", users: "Users", entities: "Entities", + classrooms: "Classrooms", "student-performance": "Student Performance", + "stats-corporate": "Stats Corporate", record: "Record", training: "Training", + vocabulary: "Vocabulary", grammar: "Grammar", "payment-record": "Payment Record", + tickets: "Tickets", "settings-platform": "Settings", settings: "Settings", + profile: "Profile", exam: "Exam", + "academic-years": "Academic Years", departments: "Departments", + admissions: "Admissions", "admission-register": "Admission Register", + "exam-sessions": "Exam Sessions", marksheets: "Marksheets", + faq: "FAQ Manager", "notification-rules": "Notification Rules", + "approval-config": "Approval Config", + "student-leave": "Student Leave", fees: "Fees & Payments", lessons: "Lessons", + gradebook: "Gradebook", "student-progress": "Student Progress", + library: "Library", activities: "Activities", facilities: "Facilities", +}; + +function AppBreadcrumbs() { + const location = useLocation(); + const segments = location.pathname.split("/").filter(Boolean); + + return ( + + + + Home + + {segments.map((seg, i) => { + const path = "/" + segments.slice(0, i + 1).join("/"); + const label = routeLabels[seg] || seg.charAt(0).toUpperCase() + seg.slice(1); + const isLast = i === segments.length - 1; + return ( + + + + {isLast ? {label} : {label}} + + + ); + })} + + + ); +} + +// ============= Main Layout ============= +export default function AdminLmsLayout() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const initials = user?.name?.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase() ?? "??"; + + const handleLogout = async () => { + await logout(); + navigate("/login"); + }; + + return ( + +
+ +
+
+
+ + +
+ +
+ + + + + + + +
+

{user?.name ?? "User"}

+

{user?.email ?? ""}

+
+ + navigate("/admin/profile")}> + Profile + + navigate("/admin/settings-platform")}> + Settings + + + + Logout + +
+
+
+
+
+ +
+
+
+ + + + Need help? + +
+ ); +} + +// ============= Admin Sidebar ============= +function AdminSidebar() { + const { state } = useSidebar(); + const collapsed = state === "collapsed"; + const { user } = useAuth(); + const { hasAnyPermission, isAdmin } = usePermissions(); + + const showManagement = isAdmin || hasAnyPermission(["view_entities", "view_students", "view_teachers"]); + const showReports = isAdmin || hasAnyPermission(["view_statistics", "view_student_performance", "view_entity_statistics"]); + const showPayments = isAdmin || hasAnyPermission(["view_payment_record", "pay_entity"]); + + return ( + + +
+ EnCoach + {!collapsed && ( + + En + Coach + + )} +
+
+ + + + + + + + {showManagement && } + {showReports && } + + + + + + + Training + {!collapsed && } + + + + + + {trainingItems.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + + + + i.url !== "/admin/payment-record")} /> + + + +
+
+ {user?.name?.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase() ?? "??"} +
+ {!collapsed && ( +
+ {user?.name ?? "User"} + {user?.email ?? ""} +
+ )} +
+
+
+ ); +} diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx new file mode 100644 index 00000000..52751a17 --- /dev/null +++ b/src/components/AppLayout.tsx @@ -0,0 +1,145 @@ +import { Outlet, useLocation, Link, useNavigate } from "react-router-dom"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/AppSidebar"; +import { + Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, + BreadcrumbPage, BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Ticket, Settings, User, LogOut, HelpCircle } from "lucide-react"; +import React from "react"; +import AiAssistantDrawer from "@/components/ai/AiAssistantDrawer"; +import AiSearchBar from "@/components/ai/AiSearchBar"; + +const routeLabels: Record = { + "dashboard": "Dashboard", + "admin": "Admin", + "assignments": "Assignments", + "examsList": "Exams List", + "exam-structures": "Exam Structures", + "rubrics": "Rubrics", + "generation": "Exam Generation", + "approval-workflows": "Approval Workflows", + "users": "Users", + "entities": "Entities", + "classrooms": "Classrooms", + "student-performance": "Student Performance", + "stats-corporate": "Stats Corporate", + "record": "Record", + "training": "Training", + "vocabulary": "Vocabulary", + "grammar": "Grammar", + "payment-record": "Payment Record", + "tickets": "Tickets", + "settings": "Settings", + "profile": "Profile", + "exam": "Exam", + "tutor": "Tutor", +}; + +function AppBreadcrumbs() { + const location = useLocation(); + const segments = location.pathname.split("/").filter(Boolean); + + return ( + + + + + Home + + + {segments.map((seg, i) => { + const path = "/" + segments.slice(0, i + 1).join("/"); + const label = routeLabels[seg] || seg.charAt(0).toUpperCase() + seg.slice(1); + const isLast = i === segments.length - 1; + return ( + + + + {isLast ? ( + {label} + ) : ( + + {label} + + )} + + + ); + })} + + + ); +} + +export default function AppLayout() { + const navigate = useNavigate(); + + return ( + +
+ +
+
+
+ + +
+ +
+ + + + + + + +
+

Admin User

+

admin@encoach.com

+
+ + navigate("/profile")}> + Profile + + navigate("/settings")}> + Settings + + + navigate("/login")}> + Logout + +
+
+
+
+
+ +
+
+
+ + {/* Floating help button */} + + + Need help? + +
+ ); +} diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx new file mode 100644 index 00000000..f557045b --- /dev/null +++ b/src/components/AppSidebar.tsx @@ -0,0 +1,158 @@ +import { + LayoutDashboard, ClipboardList, FileText, Layers, BookOpen, Wand2, + GitBranch, Users, BarChart3, Building2, History, GraduationCap, + CreditCard, Ticket, Settings, User, BookA, PenTool, LogOut +} from "lucide-react"; +import { NavLink } from "@/components/NavLink"; +import { useLocation } from "react-router-dom"; +import { + Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, + SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarHeader, SidebarFooter, SidebarSeparator, useSidebar, +} from "@/components/ui/sidebar"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { ChevronDown } from "lucide-react"; + +const mainItems = [ + { title: "Dashboard", url: "/dashboard/admin", icon: LayoutDashboard }, + { title: "Assignments", url: "/assignments", icon: ClipboardList }, + { title: "Exams List", url: "/examsList", icon: FileText }, + { title: "Exam Structures", url: "/exam-structures", icon: Layers }, + { title: "Rubrics", url: "/rubrics", icon: BookOpen }, + { title: "Generation", url: "/generation", icon: Wand2 }, + { title: "Approval Workflows", url: "/approval-workflows", icon: GitBranch }, +]; + +const managementItems = [ + { title: "Users", url: "/users", icon: Users }, + { title: "Entities", url: "/entities", icon: Building2 }, + { title: "Classrooms", url: "/classrooms", icon: GraduationCap }, +]; + +const reportItems = [ + { title: "Student Performance", url: "/student-performance", icon: BarChart3 }, + { title: "Stats Corporate", url: "/stats-corporate", icon: Building2 }, + { title: "Record", url: "/record", icon: History }, +]; + +const trainingItems = [ + { title: "Vocabulary", url: "/training/vocabulary", icon: BookA }, + { title: "Grammar", url: "/training/grammar", icon: PenTool }, +]; + +const supportItems = [ + { title: "Payment Record", url: "/payment-record", icon: CreditCard }, + { title: "Tickets", url: "/tickets", icon: Ticket }, + { title: "Settings", url: "/settings", icon: Settings }, +]; + +function SidebarNavGroup({ label, items }: { label: string; items: typeof mainItems }) { + const { state } = useSidebar(); + const collapsed = state === "collapsed"; + const location = useLocation(); + + return ( + + {label} + + + {items.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + ); +} + +export function AppSidebar() { + const { state } = useSidebar(); + const collapsed = state === "collapsed"; + const location = useLocation(); + + return ( + + +
+
+ +
+ {!collapsed && ( + + EnCoach + + )} +
+
+ + + + + + + + + + + + + Training + {!collapsed && } + + + + + + {trainingItems.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + + + + + + + + + +
+
+ +
+ {!collapsed && ( +
+ Admin User + admin@encoach.com +
+ )} +
+
+
+ ); +} diff --git a/src/components/NavLink.tsx b/src/components/NavLink.tsx new file mode 100644 index 00000000..a561a95f --- /dev/null +++ b/src/components/NavLink.tsx @@ -0,0 +1,28 @@ +import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom"; +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; + +interface NavLinkCompatProps extends Omit { + className?: string; + activeClassName?: string; + pendingClassName?: string; +} + +const NavLink = forwardRef( + ({ className, activeClassName, pendingClassName, to, ...props }, ref) => { + return ( + + cn(className, isActive && activeClassName, isPending && pendingClassName) + } + {...props} + /> + ); + }, +); + +NavLink.displayName = "NavLink"; + +export { NavLink }; diff --git a/src/components/NotificationDropdown.tsx b/src/components/NotificationDropdown.tsx new file mode 100644 index 00000000..caa9f8de --- /dev/null +++ b/src/components/NotificationDropdown.tsx @@ -0,0 +1,36 @@ +import { Bell } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +export default function NotificationDropdown() { + const notifications: { id: string; title: string; message: string; time: string; read: boolean; type: string }[] = []; + const unread = notifications.filter((n) => !n.read).length; + + return ( + + + + + +
Notifications
+ + {notifications.slice(0, 4).map((n) => ( + + {n.title} + {n.message} + {n.time} + + ))} +
+
+ ); +} diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 00000000..e97752cc --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,64 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useAuth } from "@/contexts/AuthContext"; +import { usePermissions } from "@/hooks/usePermissions"; +import type { UserRole } from "@/types/auth"; + +interface Props { + allowedRoles?: UserRole[]; + requiredPermissions?: string[]; +} + +function getRoleDashboard(role: UserRole): string { + switch (role) { + case "student": + return "/student/dashboard"; + case "teacher": + return "/teacher/dashboard"; + case "admin": + case "developer": + return "/admin/dashboard"; + case "corporate": + case "mastercorporate": + case "agent": + return "/admin/platform"; + default: + return "/login"; + } +} + +export default function ProtectedRoute({ allowedRoles, requiredPermissions }: Props) { + const { user, isAuthenticated, isLoading } = useAuth(); + const { hasAllPermissions, isLoading: permissionsLoading } = usePermissions(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated || !user) { + return ; + } + + if (allowedRoles && !allowedRoles.includes(user.user_type)) { + return ; + } + + if (requiredPermissions && requiredPermissions.length > 0) { + if (permissionsLoading) { + return ( +
+
+
+ ); + } + + if (!hasAllPermissions(requiredPermissions)) { + return ; + } + } + + return ; +} diff --git a/src/components/RoleLayout.tsx b/src/components/RoleLayout.tsx new file mode 100644 index 00000000..6c1943fc --- /dev/null +++ b/src/components/RoleLayout.tsx @@ -0,0 +1,151 @@ +import { Outlet, Link, useNavigate, useLocation } from "react-router-dom"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { + Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, + SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarHeader, SidebarFooter, SidebarSeparator, useSidebar, +} from "@/components/ui/sidebar"; +import { NavLink } from "@/components/NavLink"; +import { useAuth, UserRole } from "@/contexts/AuthContext"; +import NotificationDropdown from "@/components/NotificationDropdown"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { LogOut, User, Settings, LucideIcon } from "lucide-react"; +import React from "react"; + +export interface NavItem { + title: string; + url: string; + icon: LucideIcon; +} + +export interface NavGroup { + label: string; + items: NavItem[]; +} + +interface RoleLayoutProps { + navGroups: NavGroup[]; + role: UserRole; +} + +function SidebarNav({ navGroups }: { navGroups: NavGroup[] }) { + const { state } = useSidebar(); + const collapsed = state === "collapsed"; + + return ( + <> + {navGroups.map((group) => ( + + {group.label} + + + {group.items.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + ))} + + ); +} + +export default function RoleLayout({ navGroups, role }: RoleLayoutProps) { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const initials = user?.name?.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase() ?? "??"; + + const handleLogout = async () => { + await logout(); + navigate("/login"); + }; + + return ( + +
+ + +
+ EnCoach + + En + Coach + +
+
+ + + + + + +
+
+ {initials} +
+
+ {user?.name} + {user?.email} +
+
+
+
+ +
+
+
+ + {role} Portal +
+
+ + + + + + +
+

{user?.name}

+

{user?.email}

+
+ + navigate(`/${role}/profile`)}> + Profile + + + + Logout + +
+
+
+
+
+ +
+
+
+
+ ); +} diff --git a/src/components/StudentLayout.tsx b/src/components/StudentLayout.tsx new file mode 100644 index 00000000..3fc64e30 --- /dev/null +++ b/src/components/StudentLayout.tsx @@ -0,0 +1,51 @@ +import RoleLayout, { NavGroup } from "./RoleLayout"; +import { + LayoutDashboard, BookOpen, ClipboardList, BarChart3, + CalendarCheck, Calendar, User, Target, GraduationCap, ListChecks, + MessageSquare, Megaphone, Mail, TrendingUp, +} from "lucide-react"; + +const navGroups: NavGroup[] = [ + { + label: "Learning", + items: [ + { title: "Dashboard", url: "/student/dashboard", icon: LayoutDashboard }, + { title: "My Courses", url: "/student/courses", icon: BookOpen }, + { title: "Subject Registration", url: "/student/subject-registration", icon: ListChecks }, + { title: "Assignments", url: "/student/assignments", icon: ClipboardList }, + ], + }, + { + label: "Adaptive Learning", + items: [ + { title: "My Subjects", url: "/student/subjects", icon: Target }, + ], + }, + { + label: "Progress", + items: [ + { title: "Grades", url: "/student/grades", icon: BarChart3 }, + { title: "Attendance", url: "/student/attendance", icon: CalendarCheck }, + { title: "Timetable", url: "/student/timetable", icon: Calendar }, + { title: "My Journey", url: "/student/journey", icon: TrendingUp }, + ], + }, + { + label: "Communication", + items: [ + { title: "Discussions", url: "/student/discussions", icon: MessageSquare }, + { title: "Messages", url: "/student/messages", icon: Mail }, + { title: "Announcements", url: "/student/announcements", icon: Megaphone }, + ], + }, + { + label: "Account", + items: [ + { title: "Profile", url: "/student/profile", icon: User }, + ], + }, +]; + +export default function StudentLayout() { + return ; +} diff --git a/src/components/TeacherLayout.tsx b/src/components/TeacherLayout.tsx new file mode 100644 index 00000000..caeccaef --- /dev/null +++ b/src/components/TeacherLayout.tsx @@ -0,0 +1,42 @@ +import RoleLayout, { NavGroup } from "./RoleLayout"; +import { + LayoutDashboard, BookOpen, ClipboardList, FileText, + CalendarCheck, Users, Calendar, User, MessageSquare, + Megaphone, Wand2, +} from "lucide-react"; + +const navGroups: NavGroup[] = [ + { + label: "Teaching", + items: [ + { title: "Dashboard", url: "/teacher/dashboard", icon: LayoutDashboard }, + { title: "Courses", url: "/teacher/courses", icon: BookOpen }, + { title: "Assignments", url: "/teacher/assignments", icon: ClipboardList }, + ], + }, + { + label: "Management", + items: [ + { title: "Attendance", url: "/teacher/attendance", icon: CalendarCheck }, + { title: "Students", url: "/teacher/students", icon: Users }, + { title: "Timetable", url: "/teacher/timetable", icon: Calendar }, + ], + }, + { + label: "Communication", + items: [ + { title: "Discussions", url: "/teacher/discussions", icon: MessageSquare }, + { title: "Announcements", url: "/teacher/announcements", icon: Megaphone }, + ], + }, + { + label: "Account", + items: [ + { title: "Profile", url: "/teacher/profile", icon: User }, + ], + }, +]; + +export default function TeacherLayout() { + return ; +} diff --git a/src/components/ai/AiAlertBanner.tsx b/src/components/ai/AiAlertBanner.tsx new file mode 100644 index 00000000..e14ec370 --- /dev/null +++ b/src/components/ai/AiAlertBanner.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { AlertTriangle, X, Sparkles, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { analyticsService } from "@/services/analytics.service"; + +export default function AiAlertBanner() { + const [dismissedIds, setDismissedIds] = useState>(() => new Set()); + const [errorDismissed, setErrorDismissed] = useState(false); + + const { data: alerts, isLoading, isError, error } = useQuery({ + queryKey: ["ai", "alerts"], + queryFn: () => analyticsService.getAlerts(), + }); + + const visible = alerts?.filter((a) => !dismissedIds.has(a.id)) ?? []; + + if (isLoading) { + return ( +
+ +

Loading alerts…

+
+ ); + } + + if (isError && !errorDismissed) { + return ( +
+ +
+

+ Alerts unavailable +

+

{error instanceof Error ? error.message : "Could not load alerts."}

+
+ +
+ ); + } + + if (isError && errorDismissed) return null; + + if (!alerts?.length) { + return ( +
+ +

No AI alerts right now.

+
+ ); + } + + if (!visible.length) return null; + + return ( +
+ {visible.map((alert) => ( +
+ +
+

+ {alert.title} +

+

{alert.description}

+
+ +
+ ))} +
+ ); +} diff --git a/src/components/ai/AiAssistantDrawer.tsx b/src/components/ai/AiAssistantDrawer.tsx new file mode 100644 index 00000000..05ccaf7b --- /dev/null +++ b/src/components/ai/AiAssistantDrawer.tsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { useLocation } from "react-router-dom"; +import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Sparkles, Send, Loader2 } from "lucide-react"; +import { coachingService } from "@/services/coaching.service"; +import { useToast } from "@/hooks/use-toast"; + +const quickActions = [ + "Platform health summary", + "Show dropout risks", + "Compare course performance", + "Suggest batch improvements", +]; + +export default function AiAssistantDrawer() { + const [open, setOpen] = useState(false); + const [messages, setMessages] = useState<{ role: "user" | "ai"; text: string }[]>([]); + const [input, setInput] = useState(""); + const location = useLocation(); + const { toast } = useToast(); + + const chatMutation = useMutation({ + mutationFn: (message: string) => + coachingService.chat({ message, context: { page: location.pathname } }), + onSuccess: (data) => { + setMessages((prev) => [...prev, { role: "ai", text: data.message }]); + }, + onError: (err: Error) => { + toast({ + variant: "destructive", + title: "Could not get a reply", + description: err.message || "Something went wrong. Try again.", + }); + setMessages((prev) => [ + ...prev, + { + role: "ai", + text: "Sorry, I could not reach the assistant. Please try again in a moment.", + }, + ]); + }, + }); + + const handleSend = (text: string) => { + if (!text.trim() || chatMutation.isPending) return; + const userMsg = text.trim(); + setMessages((prev) => [...prev, { role: "user", text: userMsg }]); + setInput(""); + chatMutation.mutate(userMsg); + }; + + return ( + <> + + + + + + + + EnCoach AI Assistant + + + +
+ {quickActions.map((action) => ( + + ))} +
+ +
+ {messages.length === 0 && ( +
+ +

Ask me anything about the platform,

+

or click a quick action above.

+
+ )} + {messages.map((msg, i) => ( +
+ {msg.role === "ai" && ( + + )} + {msg.text} +
+ ))} + {chatMutation.isPending && ( +
+ + Thinking... +
+ )} +
+ +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSend(input)} + /> + +
+
+
+ + ); +} diff --git a/src/components/ai/AiBatchOptimizer.tsx b/src/components/ai/AiBatchOptimizer.tsx new file mode 100644 index 00000000..7a100796 --- /dev/null +++ b/src/components/ai/AiBatchOptimizer.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Sparkles, Loader2 } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { analyticsService } from "@/services/analytics.service"; + +interface Props { + batchId?: number; +} + +export default function AiBatchOptimizer({ batchId }: Props) { + const [open, setOpen] = useState(false); + const { toast } = useToast(); + + const mutation = useMutation({ + mutationFn: (id: number) => analyticsService.getBatchOptimization(id), + onError: (err: Error) => { + toast({ + title: "Optimization failed", + description: err.message || "Could not analyze this batch.", + variant: "destructive", + }); + }, + }); + + const handleOpen = () => { + if (batchId == null) { + toast({ + title: "No batch selected", + description: "Choose a batch to analyze, or open this from a batch detail page.", + variant: "destructive", + }); + return; + } + mutation.reset(); + setOpen(true); + mutation.mutate(batchId); + }; + + const handleApply = () => { + toast({ title: "Suggestion Applied", description: "Batch split recommendation has been saved successfully." }); + setOpen(false); + }; + + const onOpenChange = (next: boolean) => { + setOpen(next); + if (!next) mutation.reset(); + }; + + const suggestions = mutation.data ?? []; + const showResults = !mutation.isPending && !mutation.isError && suggestions.length > 0; + const showEmpty = !mutation.isPending && !mutation.isError && mutation.isSuccess && suggestions.length === 0; + + return ( + <> + + + + + + AI Batch Optimization + + + {mutation.isPending ? ( +
+ Analyzing batch data... +
+ ) : mutation.isError ? ( +

Something went wrong. Try again.

+ ) : showResults ? ( +
+
+ {suggestions.map((s, i) => ( +
+

{s.impact} impact

+

{s.suggestion}

+ {s.details ?

{s.details}

: null} +
+ ))} +
+
+ + +
+
+ ) : showEmpty ? ( +

No optimization suggestions for this batch.

+ ) : null} +
+
+ + ); +} diff --git a/src/components/ai/AiCreationAssistant.tsx b/src/components/ai/AiCreationAssistant.tsx new file mode 100644 index 00000000..84af4b7d --- /dev/null +++ b/src/components/ai/AiCreationAssistant.tsx @@ -0,0 +1,215 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Sparkles, Loader2 } from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import { lmsService } from "@/services/lms.service"; + +interface Props { + type: "course" | "assignment" | "batch" | "rubric" | "exam" | "student" | "teacher"; + trigger?: React.ReactNode; + onGenerated?: (data: any) => void; +} + +const typeLabels: Record = { + course: "Course", assignment: "Assignment", batch: "Batch", rubric: "Rubric", + exam: "Exam", student: "Student Plan", teacher: "Teacher Assignment", +}; + +export default function AiCreationAssistant({ type, trigger, onGenerated }: Props) { + const [open, setOpen] = useState(false); + const [prompt, setPrompt] = useState(""); + const [level, setLevel] = useState("b2"); + const [subjectId, setSubjectId] = useState(""); + const { toast } = useToast(); + + const generateCourseMutation = useMutation({ + mutationFn: (payload: { title: string; subject_id?: number; level?: string }) => + lmsService.aiGenerateCourse(payload), + onError: (err: Error) => { + toast({ + variant: "destructive", + title: "Generation failed", + description: err.message || "Could not generate course outline.", + }); + }, + }); + + const handleGenerate = () => { + if (type !== "course") { + toast({ + variant: "destructive", + title: "Not available", + description: "Live AI generation is wired for courses only. Use the course type to generate an outline.", + }); + return; + } + const title = prompt.trim() || "Untitled course"; + const sid = subjectId.trim() ? Number(subjectId) : undefined; + generateCourseMutation.mutate({ + title, + ...(sid !== undefined && !Number.isNaN(sid) ? { subject_id: sid } : {}), + level, + }); + }; + + const handleApply = () => { + if (!generateCourseMutation.data) return; + onGenerated?.(generateCourseMutation.data); + toast({ + title: `AI ${typeLabels[type]} Applied`, + description: `The AI-generated ${type} has been applied to your form.`, + }); + setOpen(false); + generateCourseMutation.reset(); + }; + + return ( + <> + {trigger ? ( +
setOpen(true)}>{trigger}
+ ) : ( + + )} + + { + setOpen(v); + if (!v) generateCourseMutation.reset(); + }} + > + + + + AI {typeLabels[type]} Generator + + + + {type === "course" && generateCourseMutation.isSuccess ? ( +
+
+

+ AI Generated {typeLabels[type]} +

+
+ Outline +
+                    {typeof generateCourseMutation.data?.outline === "string"
+                      ? generateCourseMutation.data.outline
+                      : JSON.stringify(generateCourseMutation.data?.outline, null, 2)}
+                  
+
+
+ +
+ + +
+
+ ) : ( +
+
+ +