diff --git a/docs/ENCOACH_ODOO19_BACKEND_SRS.md b/docs/ENCOACH_ODOO19_BACKEND_SRS.md new file mode 100644 index 0000000..2500333 --- /dev/null +++ b/docs/ENCOACH_ODOO19_BACKEND_SRS.md @@ -0,0 +1,1281 @@ +# EnCoach Platform — Odoo 19 Backend SRS + +**Version:** 1.0 +**Date:** March 2026 +**Status:** Definitive +**Purpose:** Complete backend specification for an Odoo 19 developer to build all required modules, models, and REST API endpoints for the EnCoach Adaptive Learning Platform. + +--- + +## 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](#25-approval-workflows) +26. [Non-Functional Requirements](#26-non-functional-requirements) +27. [Implementation Priority](#27-implementation-priority) + +--- + +## 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 24 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 | + +**Total: 8 OpenEduCat (ported) + 23 custom = 31 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 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 | +| `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) | + +--- + +## 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 + +### 25.1 Model (`encoach.approval.workflow`) + +| Field | Type | Description | +|-------|------|-------------| +| `name` | Char | Workflow name | +| `type` | Selection | `exam_publish`, `assignment_publish`, `content_publish` | +| `steps` | One2many | Approval steps | +| `entity_id` | Many2one | Entity | +| `active` | Boolean | Active flag | + +### 25.2 API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/approval-workflows` | JWT | Paginated workflows | +| `POST` | `/api/approval-workflows` | JWT | Create workflow | +| `PATCH` | `/api/approval-workflows/{id}` | JWT | Update workflow | +| `DELETE` | `/api/approval-workflows/{id}` | JWT | Delete workflow | + +--- + +## 26. Non-Functional Requirements + +### 26.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` | + +### 26.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 | + +### 26.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 | + +### 26.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) | + +### 26.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 ` | + +--- + +## 27. Implementation Priority + +| Priority | Modules | Rationale | +|----------|---------|-----------| +| **P0** | `encoach_core`, `encoach_api` (auth, users, entities, permissions) | Foundation — blocks everything | +| **P0** | `openeducat_core` (port to v19), `encoach_lms_api` (courses, batches, basic serialization) | LMS foundation | +| **P1** | `encoach_taxonomy`, `encoach_adaptive`, `encoach_adaptive_api`, `encoach_adaptive_ai` | Core adaptive learning loop | +| **P1** | `encoach_exam`, `encoach_ai_generation`, `encoach_ai_grading` | Exam engine | +| **P1** | `encoach_resources` | Learning content | +| **P2** | `openeducat_timetable`, `openeducat_attendance`, `openeducat_classroom` (port to v19) | LMS timetable/attendance | +| **P2** | `openeducat_admission` (port to v19) + admission endpoints | Enrollment workflow | +| **P2** | Academic years/terms, departments endpoints | Institutional calendar | +| **P2** | `encoach_ai`, `encoach_ai_media` | AI services + media | +| **P2** | `encoach_assignment`, `encoach_classroom` | Assignments + groups | +| **P3** | `openeducat_exam` (port to v19) + institutional exam endpoints | Institutional assessment | +| **P3** | `openeducat_assignment` (port to v19) + submission endpoints | Homework workflow | +| **P3** | Subject registration endpoints | Student subject selection | +| **P3** | `encoach_training`, analytics endpoints | Training + analytics | +| **P3** | `encoach_sis`, `encoach_branding` | University integration | +| **P4** | `encoach_subscription` (Stripe, PayPal, Paymob) | Payments | +| **P4** | `encoach_ticket` | Support | +| **P4** | Approval workflows, storage | Operational features | + +--- + +## Appendix A: Complete Endpoint Summary + +Total API endpoints: **200+** + +| Service Area | Endpoint Count | +|-------------|---------------| +| Auth & Users | 10 | +| Entities & Permissions | 9 | +| LMS (Courses, Batches, Timetable, Attendance, Grades) | 16 | +| Academic Years, Terms, Departments | 14 | +| Admissions | 12 | +| Subject Registration | 6 | +| EnCoach Exams (AI) | 15 | +| Institutional Exams | 21 | +| EnCoach Assignments | 7 | +| Course Assignments & Submissions | 11 | +| Classrooms & Groups | 7 | +| Taxonomy | 16 | +| Adaptive Learning | 17 | +| Resources | 7 | +| AI Coaching | 6 | +| AI Evaluation | 4 | +| AI Generation | 2 | +| AI Media | 3 | +| AI Analytics | 6 | +| Training | 3 | +| Statistics | 4 | +| Subscriptions & Payments | 8 + 3 webhooks | +| Tickets | 4 | +| Storage | 2 | +| Approval Workflows | 4 | + +--- + +## 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) +``` + +--- + +*This SRS is the definitive backend specification for the EnCoach platform on Odoo 19. It covers all 200+ REST API endpoints expected by the React frontend, all Odoo models (OpenEduCat + custom), and all AI service integrations.* diff --git a/docs/ENCOACH_UNIFIED_SRS.md b/docs/ENCOACH_UNIFIED_SRS.md index 5dced0e..73f648f 100644 --- a/docs/ENCOACH_UNIFIED_SRS.md +++ b/docs/ENCOACH_UNIFIED_SRS.md @@ -53,11 +53,19 @@ 28. [Subscriptions and Payments](#28-subscriptions-and-payments) 29. [Training Content](#29-training-content) -**Part VIII -- Technical Specifications** -30. [Data Models](#30-data-models) -31. [REST API Specification](#31-rest-api-specification) -32. [Frontend Page Inventory](#32-frontend-page-inventory) -33. [Non-Functional Requirements](#33-non-functional-requirements) +**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 IX -- Technical Specifications** +36. [Data Models](#36-data-models) +37. [REST API Specification](#37-rest-api-specification) +38. [Frontend Page Inventory](#38-frontend-page-inventory) +39. [Non-Functional Requirements](#39-non-functional-requirements) --- @@ -344,7 +352,7 @@ The current permission model is IELTS-hardcoded (`generate_reading`, `createWrit | FR-SIDE-03 | Logout ends session and clears state | | FR-SIDE-04 | Ticket badge shows assigned count | -**Student sidebar:** Dashboard, Subjects, Courses, Assignments, Grades, Attendance, Timetable, Profile +**Student sidebar:** Dashboard, Subjects, Courses, Subject Registration, Assignments, Grades, Attendance, Timetable, Profile **Teacher sidebar:** Dashboard, Courses, Assignments, Attendance, Students, Timetable, Profile @@ -354,6 +362,7 @@ The current permission model is IELTS-hardcoded (`generate_reading`, `createWrit |-------|-------| | **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 | @@ -962,11 +971,469 @@ Each simulated AI component in the new frontend maps to a real backend service: --- -# Part VIII -- Technical Specifications +# Part VIII -- Institutional LMS (OpenEduCat) -## 30. Data Models +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.1 Existing Models (from `ielts-be-v0`) +--- + +## 30. Academic Year and Term Management + +### 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 + +### 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 + +### 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 + +### 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 + +### 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 + +### 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 IX -- Technical Specifications + +## 36. Data Models + +### 36.1 Existing Models (from `ielts-be-v0`) | Model | Module | Purpose | |-------|--------|---------| @@ -994,7 +1461,7 @@ Each simulated AI component in the new frontend maps to a real backend service: | `encoach.elai.avatar` | `encoach_ai_media` | ELAI avatar profiles | | `encoach.registration` | `encoach_registration` | Batch registration mixin | -### 30.2 New Models (Adaptive Learning Engine) +### 36.2 New Models (Adaptive Learning Engine) | Model | Module | Purpose | Key Fields | |-------|--------|---------|------------| @@ -1009,14 +1476,14 @@ Each simulated AI component in the new frontend maps to a real backend service: | `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` | -### 30.3 New Models (SIS Integration) +### 36.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 | -### 30.4 New Models (Branding) +### 36.4 New Models (Branding) | Model | Module | Purpose | |-------|--------|---------| @@ -1024,9 +1491,9 @@ Each simulated AI component in the new frontend maps to a real backend service: --- -## 31. REST API Specification +## 37. REST API Specification -### 31.1 Existing Endpoints (81+) +### 37.1 Existing Endpoints (81+) All existing endpoints from the Odoo 19 backend remain. Key groups: @@ -1049,7 +1516,7 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | Storage | `/api/storage` | 2 | | Approvals | `/api/approval-workflows` | 3 | -### 31.2 New Endpoints (Adaptive Learning Engine, 40+) +### 37.2 New Endpoints (Adaptive Learning Engine, 40+) | Group | Method | Path | Description | |-------|--------|------|-------------| @@ -1101,7 +1568,7 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | | `GET` | `/api/analytics/subject` | Subject analytics | | | `GET` | `/api/analytics/content-gaps` | Content gap report | -### 31.3 New Endpoints (LMS API Bridge) +### 37.3 New Endpoints (LMS API Bridge) | Method | Path | Description | |--------|------|-------------| @@ -1114,7 +1581,7 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | `GET/POST` | `/api/attendance` | Attendance records | | `GET` | `/api/grades` | Gradebook | -### 31.4 New Endpoints (SIS Integration) +### 37.4 New Endpoints (SIS Integration) | Method | Path | Description | |--------|------|-------------| @@ -1123,11 +1590,61 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | `GET` | `/api/sis/mappings` | Field mappings | | `PATCH` | `/api/sis/mappings` | Update mappings | +### 37.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 | + --- -## 32. Frontend Page Inventory +## 38. Frontend Page Inventory -### 32.1 Auth Pages (3) +### 38.1 Auth Pages (3) | Route | Page | API Dependencies | |-------|------|-----------------| @@ -1135,7 +1652,7 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | `/register` | Register | `POST /api/register` | | `/forgot-password` | Forgot Password | `POST /api/reset/sendVerification` | -### 32.2 Student Pages (10+) +### 38.2 Student Pages (12+) | Route | Page | Key APIs | AI Components | |-------|------|----------|---------------| @@ -1152,8 +1669,9 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | `/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` | -- | -### 32.3 Teacher Pages (10) +### 38.3 Teacher Pages (10) | Route | Page | Key APIs | AI Components | |-------|------|----------|---------------| @@ -1168,7 +1686,14 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | `/teacher/timetable` | Timetable | `/api/timetable` | -- | | `/teacher/profile` | Profile | `/api/user` | -- | -### 32.4 Admin Pages (30+) +### 38.4 Public Pages (2) + +| Route | Page | Key APIs | AI Components | +|-------|------|----------|---------------| +| `/apply` | **NEW:** Admission Application | `/api/admissions`, `/api/admission-registers` | -- | +| `/apply/status` | **NEW:** Application Status | `/api/admissions/{id}` | -- | + +### 38.5 Admin Pages (38+) | Route | Page | Key APIs | AI Components | |-------|------|----------|---------------| @@ -1204,10 +1729,17 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | `/admin/settings-platform` | Platform Settings | Settings APIs | -- | | `/admin/exam` | Exam Runner | `/api/exam`, `/api/sessions` | -- | | `/admin/profile` | Admin Profile | `/api/user` | -- | +| `/admin/academic-years` | **NEW:** Academic Year Manager | `/api/academic-years`, `/api/academic-terms` | -- | +| `/admin/departments` | **NEW:** Department Manager | `/api/departments` | -- | +| `/admin/admissions` | **NEW:** Admission List | `/api/admissions` | -- | +| `/admin/admissions/:id` | **NEW:** Admission Detail | `/api/admissions/{id}` | -- | +| `/admin/admission-register` | **NEW:** Admission Register | `/api/admission-registers` | -- | +| `/admin/exam-sessions` | **NEW:** Institutional Exam Sessions | `/api/inst-exam-sessions`, `/api/inst-exams` | -- | +| `/admin/marksheets` | **NEW:** Marksheet Manager | `/api/marksheets`, `/api/result-templates`, `/api/grade-config` | -- | --- -## 33. Non-Functional Requirements +## 39. Non-Functional Requirements | ID | Requirement | |----|-------------| @@ -1258,11 +1790,19 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | 18 | `encoach_adaptive` | **NEW** | Proficiency, learning plans, progress tracking, content cache | | 19 | `encoach_adaptive_api` | **NEW** | REST controllers for adaptive learning | | 20 | `encoach_adaptive_ai` | **NEW** | AI services for diagnostics, plans, coaching, content | -| 21 | `encoach_lms_api` | **NEW** | REST API bridge for OpenEduCat models | +| 21 | `encoach_lms_api` | **NEW** | REST API bridge for OpenEduCat models (extends `op.course`, `op.batch`, adds missing fields) | | 22 | `encoach_sis` | **NEW** | UTAS SIS integration (sync, mapping) | | 23 | `encoach_branding` | **NEW** | Whitelabeling configuration | +| 24 | `openeducat_core` | **OpenEduCat (port to v19)** | `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` | +| 25 | `openeducat_timetable` | **OpenEduCat (port to v19)** | `op.session`, `op.timing` | +| 26 | `openeducat_attendance` | **OpenEduCat (port to v19)** | `op.attendance.register`, `op.attendance.sheet`, `op.attendance.line`, `op.attendance.type` | +| 27 | `openeducat_classroom` | **OpenEduCat (port to v19)** | `op.classroom` (physical rooms) | +| 28 | `openeducat_exam` | **OpenEduCat (port to v19)** | `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` | +| 29 | `openeducat_assignment` | **OpenEduCat (port to v19)** | `op.assignment`, `op.assignment.sub.line`, `grading.assignment`, `grading.assignment.type` | +| 30 | `openeducat_admission` | **OpenEduCat (port to v19)** | `op.admission`, `op.admission.register` | +| 31 | `openeducat_facility` | **OpenEduCat (port to v19)** | `op.facility`, `op.facility.line` (dependency of classroom) | -**Total: 15 existing + 8 new = 23 modules** +**Total: 15 existing EnCoach + 8 new EnCoach + 8 OpenEduCat = 31 modules** --- @@ -1273,11 +1813,14 @@ All existing endpoints from the Odoo 19 backend remain. Key groups: | **P0** | Auth, core user management, taxonomy models, resource model | Platform foundation; blocks everything | | **P1** | Diagnostic engine, proficiency profile, learning plan generation | Core adaptive learning loop | | **P1** | Exam engine with multi-subject support | Exam generation and delivery | -| **P2** | LMS API bridge (OpenEduCat), course/batch/timetable/attendance | LMS functionality | +| **P2** | OpenEduCat porting (v17→v19), LMS API bridge, course/batch/timetable/attendance | LMS functionality | +| **P2** | Academic years/terms, departments, admission workflow | Institutional operations | | **P2** | Content delivery (hybrid), practice questions, mastery quizzes | Learning content consumption | | **P2** | AI components wiring (all 15 frontend components to real backend) | AI integration | | **P3** | AI coaching (hints, explanations, study suggestions) | Learning enhancement | | **P3** | Spaced repetition, analytics dashboards | Progress optimization | +| **P3** | Institutional exam sessions, marksheets, subject registration | Institutional assessment | +| **P3** | Course assignment submissions (file upload, grading) | Homework workflow | | **P3** | SIS integration, whitelabeling | University integration | | **P4** | Content gap detection, FAISS tips per subject, batch optimizer | Operational efficiency | | **P4** | Payment system, subscription management | Commercial features | diff --git a/src/App.tsx b/src/App.tsx index 059befa..c1eff7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,6 +70,15 @@ 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 AdmissionApplication from "@/pages/AdmissionApplication"; +import SubjectRegistrationPage from "@/pages/student/SubjectRegistrationPage"; import NotFound from "@/pages/NotFound"; import { queryClient } from "@/lib/query-client"; @@ -85,6 +94,8 @@ const App = () => ( } /> } /> } /> + {/* Public admission */} + } /> {/* Student routes */} }> @@ -102,6 +113,7 @@ const App = () => ( } /> } /> } /> + } /> @@ -163,6 +175,14 @@ const App = () => ( } /> } /> } /> + {/* Institutional LMS pages */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/components/AdminLmsLayout.tsx b/src/components/AdminLmsLayout.tsx index d299fa4..58095d4 100644 --- a/src/components/AdminLmsLayout.tsx +++ b/src/components/AdminLmsLayout.tsx @@ -26,6 +26,7 @@ import { FileText, Layers, BookOpen, Wand2, GitBranch, Users, BarChart3, Building2, History, CreditCard, Ticket, BookA, PenTool, Calendar, ChevronDown, HelpCircle, LucideIcon, Target, FolderOpen, + CalendarDays, Landmark, UserPlus, ScrollText, Award, } from "lucide-react"; import React from "react"; @@ -60,6 +61,15 @@ const adaptiveItems: NavItem[] = [ { 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 }, +]; + const managementItems: NavItem[] = [ { title: "Users", url: "/admin/users", icon: Users }, { title: "Entities", url: "/admin/entities", icon: Building2 }, @@ -126,6 +136,9 @@ const routeLabels: Record = { 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", }; function AppBreadcrumbs() { @@ -257,6 +270,7 @@ function AdminSidebar() { + {showManagement && } {showReports && } diff --git a/src/components/StudentLayout.tsx b/src/components/StudentLayout.tsx index a427edc..398e355 100644 --- a/src/components/StudentLayout.tsx +++ b/src/components/StudentLayout.tsx @@ -1,7 +1,7 @@ import RoleLayout, { NavGroup } from "./RoleLayout"; import { LayoutDashboard, BookOpen, ClipboardList, BarChart3, - CalendarCheck, Calendar, User, Target, GraduationCap, + CalendarCheck, Calendar, User, Target, GraduationCap, ListChecks, } from "lucide-react"; const navGroups: NavGroup[] = [ @@ -10,6 +10,7 @@ const navGroups: NavGroup[] = [ 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 }, ], }, diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 6cbb0c0..97da6a9 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -5,4 +5,7 @@ export * from "./useExams"; export * from "./useAssignments"; export * from "./useEntities"; export * from "./useAdaptive"; +export * from "./useAcademic"; +export * from "./useAdmissions"; +export * from "./useInstitutionalExams"; export { usePermissions } from "../usePermissions"; diff --git a/src/hooks/queries/keys.ts b/src/hooks/queries/keys.ts index 8e70455..c677bf3 100644 --- a/src/hooks/queries/keys.ts +++ b/src/hooks/queries/keys.ts @@ -92,4 +92,65 @@ export const queryKeys = { contentGaps: (params?: Record) => ["analytics", "content-gaps", params] as const, alerts: ["analytics", "alerts"] as const, }, + academicYears: { + all: ["academic-years"] as const, + list: (params?: Record) => ["academic-years", "list", params] as const, + detail: (id: number) => ["academic-years", id] as const, + }, + academicTerms: { + all: ["academic-terms"] as const, + list: (params?: Record) => ["academic-terms", "list", params] as const, + detail: (id: number) => ["academic-terms", id] as const, + }, + departments: { + all: ["departments"] as const, + list: (params?: Record) => ["departments", "list", params] as const, + detail: (id: number) => ["departments", id] as const, + }, + admissionRegisters: { + all: ["admission-registers"] as const, + list: (params?: Record) => ["admission-registers", "list", params] as const, + detail: (id: number) => ["admission-registers", id] as const, + }, + admissions: { + all: ["admissions"] as const, + list: (params?: Record) => ["admissions", "list", params] as const, + detail: (id: number) => ["admissions", id] as const, + }, + instExamSessions: { + all: ["inst-exam-sessions"] as const, + list: (params?: Record) => ["inst-exam-sessions", "list", params] as const, + detail: (id: number) => ["inst-exam-sessions", id] as const, + }, + examTypes: { + all: ["exam-types"] as const, + list: (params?: Record) => ["exam-types", "list", params] as const, + }, + gradeConfigs: { + all: ["grade-configs"] as const, + list: (params?: Record) => ["grade-configs", "list", params] as const, + }, + resultTemplates: { + all: ["result-templates"] as const, + list: (params?: Record) => ["result-templates", "list", params] as const, + }, + marksheets: { + all: ["marksheets"] as const, + list: (params?: Record) => ["marksheets", "list", params] as const, + detail: (id: number) => ["marksheets", id] as const, + }, + courseAssignments: { + all: ["course-assignments"] as const, + list: (params?: Record) => ["course-assignments", "list", params] as const, + detail: (id: number) => ["course-assignments", id] as const, + }, + assignmentTypes: { + all: ["assignment-types"] as const, + list: (params?: Record) => ["assignment-types", "list", params] as const, + }, + subjectRegistrations: { + all: ["subject-registrations"] as const, + list: (params?: Record) => ["subject-registrations", "list", params] as const, + available: (params?: Record) => ["subject-registrations", "available", params] as const, + }, }; diff --git a/src/hooks/queries/useAcademic.ts b/src/hooks/queries/useAcademic.ts new file mode 100644 index 0000000..ff59fae --- /dev/null +++ b/src/hooks/queries/useAcademic.ts @@ -0,0 +1,104 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "./keys"; +import { academicService } from "@/services/academic.service"; +import type { PaginationParams } from "@/types"; +import type { + AcademicYearCreateRequest, + AcademicTermCreateRequest, + DepartmentCreateRequest, +} from "@/types/academic"; + +export function useAcademicYears(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.academicYears.list(params), + queryFn: () => academicService.listAcademicYears(params), + }); +} + +export function useAcademicYear(id: number) { + return useQuery({ + queryKey: queryKeys.academicYears.detail(id), + queryFn: () => academicService.getAcademicYear(id), + enabled: !!id, + }); +} + +export function useAcademicTerms(yearId?: number) { + return useQuery({ + queryKey: queryKeys.academicTerms.list({ year_id: yearId }), + queryFn: () => academicService.listTerms({ year_id: yearId }), + }); +} + +export function useDepartments(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.departments.list(params), + queryFn: () => academicService.listDepartments(params), + }); +} + +export function useCreateAcademicYear() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: AcademicYearCreateRequest) => academicService.createAcademicYear(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicYears.all }), + }); +} + +export function useUpdateAcademicYear() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: Partial }) => + academicService.updateAcademicYear(id, data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicYears.all }), + }); +} + +export function useDeleteAcademicYear() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => academicService.deleteAcademicYear(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicYears.all }), + }); +} + +export function useGenerateTerms() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (yearId: number) => academicService.generateTerms(yearId), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicTerms.all }), + }); +} + +export function useCreateAcademicTerm() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: AcademicTermCreateRequest) => academicService.createTerm(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.academicTerms.all }), + }); +} + +export function useCreateDepartment() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: DepartmentCreateRequest) => academicService.createDepartment(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.departments.all }), + }); +} + +export function useUpdateDepartment() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: Partial }) => + academicService.updateDepartment(id, data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.departments.all }), + }); +} + +export function useDeleteDepartment() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => academicService.deleteDepartment(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.departments.all }), + }); +} diff --git a/src/hooks/queries/useAdmissions.ts b/src/hooks/queries/useAdmissions.ts new file mode 100644 index 0000000..247747e --- /dev/null +++ b/src/hooks/queries/useAdmissions.ts @@ -0,0 +1,94 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "./keys"; +import { admissionService } from "@/services/admission.service"; +import type { PaginationParams } from "@/types"; +import type { + AdmissionRegisterCreateRequest, + AdmissionCreateRequest, +} from "@/types/admission"; + +export function useAdmissionRegisters(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.admissionRegisters.list(params), + queryFn: () => admissionService.listRegisters(params), + }); +} + +export function useAdmissions(params?: PaginationParams & { state?: string; register_id?: number; course_id?: number }) { + return useQuery({ + queryKey: queryKeys.admissions.list(params), + queryFn: () => admissionService.listAdmissions(params), + }); +} + +export function useAdmission(id: number) { + return useQuery({ + queryKey: queryKeys.admissions.detail(id), + queryFn: () => admissionService.getAdmission(id), + enabled: !!id, + }); +} + +export function useCreateAdmissionRegister() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: AdmissionRegisterCreateRequest) => admissionService.createRegister(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissionRegisters.all }), + }); +} + +export function useConfirmRegister() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => admissionService.confirmRegister(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissionRegisters.all }), + }); +} + +export function useCloseRegister() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => admissionService.closeRegister(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissionRegisters.all }), + }); +} + +export function useCreateAdmission() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: AdmissionCreateRequest) => admissionService.createAdmission(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }), + }); +} + +export function useSubmitAdmission() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => admissionService.submitAdmission(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }), + }); +} + +export function useConfirmAdmission() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => admissionService.confirmAdmission(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }), + }); +} + +export function useAdmitAdmission() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => admissionService.admitAdmission(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }), + }); +} + +export function useRejectAdmission() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => admissionService.rejectAdmission(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.admissions.all }), + }); +} diff --git a/src/hooks/queries/useInstitutionalExams.ts b/src/hooks/queries/useInstitutionalExams.ts new file mode 100644 index 0000000..d37b193 --- /dev/null +++ b/src/hooks/queries/useInstitutionalExams.ts @@ -0,0 +1,199 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "./keys"; +import { institutionalExamService } from "@/services/institutional-exam.service"; +import type { PaginationParams } from "@/types"; +import type { + InstExamSessionCreateRequest, + InstExamCreateRequest, + ResultTemplateCreateRequest, + CourseAssignmentCreateRequest, + SubjectRegistrationCreateRequest, + AssignmentSubmission, +} from "@/types/institutional-exam"; + +export function useInstitutionalExamSessions(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.instExamSessions.list(params), + queryFn: () => institutionalExamService.listSessions(params), + }); +} + +export function useInstitutionalExamSession(id: number) { + return useQuery({ + queryKey: queryKeys.instExamSessions.detail(id), + queryFn: () => institutionalExamService.getSession(id), + enabled: !!id, + }); +} + +export function useExamTypes(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.examTypes.list(params), + queryFn: () => institutionalExamService.listExamTypes(params), + }); +} + +export function useGradeConfigs(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.gradeConfigs.list(params), + queryFn: () => institutionalExamService.listGradeConfigs(params), + }); +} + +export function useResultTemplates(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.resultTemplates.list(params), + queryFn: () => institutionalExamService.listResultTemplates(params), + }); +} + +export function useMarksheets(params?: PaginationParams) { + return useQuery({ + queryKey: queryKeys.marksheets.list(params), + queryFn: () => institutionalExamService.listMarksheets(params), + }); +} + +export function useMarksheet(id: number) { + return useQuery({ + queryKey: queryKeys.marksheets.detail(id), + queryFn: () => institutionalExamService.getMarksheet(id), + enabled: !!id, + }); +} + +export function useCourseAssignments(params?: PaginationParams & { course_id?: number; batch_id?: number; state?: string }) { + return useQuery({ + queryKey: queryKeys.courseAssignments.list(params), + queryFn: () => institutionalExamService.listCourseAssignments(params), + }); +} + +export function useCourseAssignment(id: number) { + return useQuery({ + queryKey: queryKeys.courseAssignments.detail(id), + queryFn: () => institutionalExamService.getCourseAssignment(id), + enabled: !!id, + }); +} + +export function useSubjectRegistrations(params?: PaginationParams & { student_id?: number; course_id?: number; state?: string }) { + return useQuery({ + queryKey: queryKeys.subjectRegistrations.list(params), + queryFn: () => institutionalExamService.listSubjectRegistrations(params), + }); +} + +export function useAvailableSubjects(params?: { student_id?: number; course_id?: number }) { + return useQuery({ + queryKey: queryKeys.subjectRegistrations.available(params), + queryFn: () => institutionalExamService.getAvailableSubjects(params), + }); +} + +export function useCreateExamSession() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: InstExamSessionCreateRequest) => institutionalExamService.createSession(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }), + }); +} + +export function useScheduleExamSession() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => institutionalExamService.scheduleSession(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }), + }); +} + +export function useCreateInstitutionalExam() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: InstExamCreateRequest) => institutionalExamService.createExam(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }), + }); +} + +export function useEnterMarks() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ examId, data }: { examId: number; data: { marks: { student_id: number; score: number }[] } }) => + institutionalExamService.enterMarks(examId, data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.instExamSessions.all }), + }); +} + +export function useCreateResultTemplate() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: ResultTemplateCreateRequest) => institutionalExamService.createResultTemplate(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.resultTemplates.all }), + }); +} + +export function useGenerateMarksheets() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (templateId: number) => institutionalExamService.generateMarksheets(templateId), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.marksheets.all }), + }); +} + +export function useValidateMarksheet() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => institutionalExamService.validateMarksheet(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.marksheets.all }), + }); +} + +export function useCreateCourseAssignment() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: CourseAssignmentCreateRequest) => institutionalExamService.createCourseAssignment(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }), + }); +} + +export function usePublishCourseAssignment() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => institutionalExamService.publishCourseAssignment(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }), + }); +} + +export function useSubmitAssignment() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ assignmentId, file }: { assignmentId: number; file: File }) => + institutionalExamService.submitAssignment(assignmentId, file), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }), + }); +} + +export function useGradeSubmission() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ submissionId, data }: { submissionId: number; data: Partial }) => + institutionalExamService.gradeSubmission(submissionId, data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.courseAssignments.all }), + }); +} + +export function useCreateSubjectRegistration() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: SubjectRegistrationCreateRequest) => institutionalExamService.createSubjectRegistration(data), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.subjectRegistrations.all }), + }); +} + +export function useConfirmSubjectRegistration() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: number) => institutionalExamService.confirmSubjectRegistration(id), + onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.subjectRegistrations.all }), + }); +} diff --git a/src/pages/AdmissionApplication.tsx b/src/pages/AdmissionApplication.tsx new file mode 100644 index 0000000..94a3db5 --- /dev/null +++ b/src/pages/AdmissionApplication.tsx @@ -0,0 +1,205 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { CheckCircle2, Loader2 } from "lucide-react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { admissionService } from "@/services/admission.service"; +import { useToast } from "@/hooks/use-toast"; +import type { AdmissionCreateRequest } from "@/types/admission"; + +type Step = "register" | "personal" | "review" | "success"; + +export default function AdmissionApplication() { + const { toast } = useToast(); + const [step, setStep] = useState("register"); + const [selectedRegisterId, setSelectedRegisterId] = useState(0); + const [form, setForm] = useState>({ + first_name: "", + last_name: "", + email: "", + gender: "m", + }); + const [applicationNumber, setApplicationNumber] = useState(""); + + const { data: registersData, isLoading: loadingRegisters } = useQuery({ + queryKey: ["admission-registers", "list"], + queryFn: () => admissionService.listRegisters(), + }); + const registers = registersData?.items ?? []; + const selectedRegister = registers.find((r: { id: number }) => r.id === selectedRegisterId); + + const submitMutation = useMutation({ + mutationFn: async () => { + const admission = await admissionService.createAdmission({ ...form, register_id: selectedRegisterId }); + await admissionService.submitAdmission(admission.id); + return admission; + }, + onSuccess: (data) => { + setApplicationNumber(data.application_number); + setStep("success"); + }, + onError: () => toast({ title: "Error", description: "Submission failed. Please try again.", variant: "destructive" }), + }); + + const canProceedFromRegister = selectedRegisterId > 0; + const canProceedFromPersonal = form.first_name && form.last_name && form.email; + + return ( +
+
+ {step !== "success" && ( +
+ {["register", "personal", "review"].map((s, i) => ( +
+
i ? "bg-primary/20 text-primary" : "bg-muted text-muted-foreground" + }`}> + {i + 1} +
+ {i < 2 &&
} +
+ ))} +
+ )} + + {step === "register" && ( + + + Select Course + Choose the admission register you want to apply to. + + + {loadingRegisters ? ( +
+ ) : ( +
+ {registers + .filter((r: { state: string }) => r.state === "confirm") + .map((r: { id: number; name: string; course_name: string; start_date: string; end_date: string }) => ( + + ))} + {registers.filter((r: { state: string }) => r.state === "confirm").length === 0 && ( +

No open admissions available.

+ )} +
+ )} + +
+
+ )} + + {step === "personal" && ( + + + Personal Information + Fill in your personal details. + + +
+
+ + setForm({ ...form, first_name: e.target.value })} /> +
+
+ + setForm({ ...form, last_name: e.target.value })} /> +
+
+
+ + setForm({ ...form, middle_name: e.target.value || undefined })} /> +
+
+ + setForm({ ...form, email: e.target.value })} /> +
+
+ + setForm({ ...form, phone: e.target.value || undefined })} /> +
+
+
+ + setForm({ ...form, birth_date: e.target.value || undefined })} /> +
+
+ + +
+
+
+ + +
+
+
+ )} + + {step === "review" && ( + + + Review & Submit + Please review your application before submitting. + + +
+

Course

+

{selectedRegister?.name} — {selectedRegister?.course_name}

+
+
+

Applicant

+

{form.first_name} {form.middle_name ?? ""} {form.last_name}

+

{form.email} {form.phone ? `· ${form.phone}` : ""}

+

{form.gender === "m" ? "Male" : form.gender === "f" ? "Female" : "Other"} {form.birth_date ? `· ${form.birth_date}` : ""}

+
+
+ + +
+
+
+ )} + + {step === "success" && ( + + + +
+

Application Submitted!

+

Your application has been received.

+
+
+

Application Number

+

{applicationNumber}

+
+

Please save this number for future reference.

+
+
+ )} +
+
+ ); +} diff --git a/src/pages/admin/AcademicYearManager.tsx b/src/pages/admin/AcademicYearManager.tsx new file mode 100644 index 0000000..7f89100 --- /dev/null +++ b/src/pages/admin/AcademicYearManager.tsx @@ -0,0 +1,208 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, Trash2, ChevronRight, RefreshCw, Loader2 } from "lucide-react"; +import { + useAcademicYears, + useCreateAcademicYear, + useDeleteAcademicYear, + useGenerateTerms, + useAcademicTerms, +} from "@/hooks/queries/useAcademic"; +import { useToast } from "@/hooks/use-toast"; +import type { AcademicYear, AcademicYearCreateRequest } from "@/types/academic"; + +const TERM_STRUCTURES: { value: AcademicYear["term_structure"]; label: string }[] = [ + { value: "two_sem", label: "Two Semesters" }, + { value: "two_sem_qua", label: "Two Semesters + Quarter" }, + { value: "three_sem", label: "Three Semesters" }, + { value: "four_Quarter", label: "Four Quarters" }, + { value: "final_year", label: "Final Year" }, + { value: "others", label: "Others" }, +]; + +export default function AcademicYearManager() { + const { toast } = useToast(); + const { data: yearsData, isLoading } = useAcademicYears(); + const years = yearsData?.items ?? []; + const createYear = useCreateAcademicYear(); + const deleteYear = useDeleteAcademicYear(); + const generateTerms = useGenerateTerms(); + const [showCreate, setShowCreate] = useState(false); + const [expandedId, setExpandedId] = useState(null); + const [form, setForm] = useState({ + name: "", + start_date: "", + end_date: "", + term_structure: "two_sem", + }); + + const { data: termsData } = useAcademicTerms(expandedId ?? undefined); + const terms = termsData?.items ?? []; + + const handleCreate = () => { + createYear.mutate(form, { + onSuccess: () => { + toast({ title: "Academic year created" }); + setShowCreate(false); + setForm({ name: "", start_date: "", end_date: "", term_structure: "two_sem" }); + }, + onError: () => toast({ title: "Error", description: "Failed to create academic year", variant: "destructive" }), + }); + }; + + const handleDelete = (id: number) => { + deleteYear.mutate(id, { + onSuccess: () => toast({ title: "Academic year deleted" }), + onError: () => toast({ title: "Error", description: "Failed to delete", variant: "destructive" }), + }); + }; + + const handleGenerateTerms = (yearId: number) => { + generateTerms.mutate(yearId, { + onSuccess: () => toast({ title: "Terms generated" }), + onError: () => toast({ title: "Error", description: "Failed to generate terms", variant: "destructive" }), + }); + }; + + if (isLoading) return
; + + return ( +
+
+
+

Academic Years

+

Manage academic years and their terms.

+
+ + + + + + Create Academic Year +
+
+ + setForm({ ...form, name: e.target.value })} /> +
+
+
+ + setForm({ ...form, start_date: e.target.value })} /> +
+
+ + setForm({ ...form, end_date: e.target.value })} /> +
+
+
+ + +
+ +
+
+
+
+ + + + + + + + Name + Start Date + End Date + Term Structure + Terms + Actions + + + + {years.map((year: AcademicYear) => ( + setExpandedId(open ? year.id : null)}> + <> + + + + + + + {year.name} + {year.start_date} + {year.end_date} + + {TERM_STRUCTURES.find(t => t.value === year.term_structure)?.label ?? year.term_structure} + + + {year.terms?.length ?? 0} + + +
+ + +
+
+
+ + + + {expandedId === year.id && ( +
+

Terms

+ {terms.length > 0 ? ( +
+ {terms.map(term => ( +
+ {term.name} + {term.term_start_date} — {term.term_end_date} +
+ ))} +
+ ) : ( +

No terms yet. Click "Generate Terms" to auto-create.

+ )} +
+ )} +
+
+
+ +
+ ))} + {years.length === 0 && ( + + No academic years found. + + )} +
+
+
+
+
+ ); +} diff --git a/src/pages/admin/AdmissionDetail.tsx b/src/pages/admin/AdmissionDetail.tsx new file mode 100644 index 0000000..eb16fdf --- /dev/null +++ b/src/pages/admin/AdmissionDetail.tsx @@ -0,0 +1,173 @@ +import { useParams, useNavigate } from "react-router-dom"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { ArrowLeft, Loader2 } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { admissionService } from "@/services/admission.service"; +import { useToast } from "@/hooks/use-toast"; + +const stateBadgeVariant: Record = { + draft: "outline", + submit: "secondary", + confirm: "default", + admission: "default", + reject: "destructive", + cancel: "destructive", + done: "secondary", +}; + +export default function AdmissionDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { toast } = useToast(); + const qc = useQueryClient(); + const admissionId = Number(id); + + const { data: admission, isLoading } = useQuery({ + queryKey: ["admissions", admissionId], + queryFn: () => admissionService.getAdmission(admissionId), + enabled: !!admissionId, + }); + + const makeTransition = (action: "submit" | "confirm" | "admit" | "reject") => { + const fns = { + submit: admissionService.submitAdmission, + confirm: admissionService.confirmAdmission, + admit: admissionService.admitAdmission, + reject: admissionService.rejectAdmission, + }; + return fns[action]; + }; + + const transitionMutation = useMutation({ + mutationFn: ({ action }: { action: "submit" | "confirm" | "admit" | "reject" }) => + makeTransition(action)(admissionId), + onSuccess: (_, { action }) => { + qc.invalidateQueries({ queryKey: ["admissions"] }); + toast({ title: `Admission ${action}${action === "submit" ? "ted" : action === "reject" ? "ed" : "ed"}` }); + }, + onError: () => toast({ title: "Error", description: "Action failed", variant: "destructive" }), + }); + + if (isLoading) return
; + if (!admission) return
Admission not found.
; + + return ( +
+
+ +
+

{admission.first_name} {admission.last_name}

+

Application #{admission.application_number}

+
+ + {admission.state} + +
+ +
+ + Applicant Information + +
+
+
Full Name
+
{admission.first_name} {admission.middle_name ?? ""} {admission.last_name}
+
+
+
Email
+
{admission.email}
+
+
+
Phone
+
{admission.phone ?? "—"}
+
+
+
Birth Date
+
{admission.birth_date ?? "—"}
+
+
+
Gender
+
{admission.gender === "m" ? "Male" : admission.gender === "f" ? "Female" : "Other"}
+
+
+
Nationality
+
{admission.nationality_name ?? "—"}
+
+
+
+
+ + + Application Details + +
+
+
Course
+
{admission.course_name}
+
+
+
Register
+
{admission.register_name}
+
+
+
Batch
+
{admission.batch_name ?? "—"}
+
+
+
Fees
+
{admission.fees}
+
+
+
Applied On
+
{new Date(admission.created_at).toLocaleDateString()}
+
+
+
+
+
+ + + Actions + +
+ {admission.state === "draft" && ( + + )} + {admission.state === "submit" && ( + <> + + + + )} + {admission.state === "confirm" && ( + <> + + + + )} + {["admission", "reject", "done"].includes(admission.state) && ( +

No further actions available.

+ )} +
+
+
+
+ ); +} diff --git a/src/pages/admin/AdmissionList.tsx b/src/pages/admin/AdmissionList.tsx new file mode 100644 index 0000000..fa5b1fd --- /dev/null +++ b/src/pages/admin/AdmissionList.tsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Search } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { admissionService } from "@/services/admission.service"; +import type { Admission, AdmissionState } from "@/types/admission"; + +const STATE_TABS: { value: string; label: string }[] = [ + { value: "all", label: "All" }, + { value: "draft", label: "Draft" }, + { value: "submit", label: "Submitted" }, + { value: "confirm", label: "Confirmed" }, + { value: "admission", label: "Admitted" }, + { value: "reject", label: "Rejected" }, +]; + +const stateBadgeVariant: Record = { + draft: "outline", + submit: "secondary", + confirm: "default", + admission: "default", + reject: "destructive", + cancel: "destructive", + done: "secondary", +}; + +export default function AdmissionList() { + const navigate = useNavigate(); + const [search, setSearch] = useState(""); + const [stateFilter, setStateFilter] = useState("all"); + + const { data, isLoading } = useQuery({ + queryKey: ["admissions", "list", { search, state: stateFilter === "all" ? undefined : stateFilter }], + queryFn: () => admissionService.listAdmissions({ + search: search || undefined, + state: stateFilter === "all" ? undefined : stateFilter, + } as Parameters[0]), + }); + const admissions = data?.items ?? []; + + if (isLoading) return
; + + return ( +
+
+

Admissions

+

View and manage student admission applications.

+
+ +
+ {STATE_TABS.map(tab => ( + + ))} +
+ + + +
+ + setSearch(e.target.value)} className="pl-9" /> +
+
+ + + + + Application # + Name + Course + Register + State + Date + + + + {admissions.map((adm: Admission) => ( + navigate(`/admin/admissions/${adm.id}`)}> + {adm.application_number} + {adm.first_name} {adm.last_name} + {adm.course_name} + {adm.register_name} + + {adm.state} + + {new Date(adm.created_at).toLocaleDateString()} + + ))} + {admissions.length === 0 && ( + + No admissions found. + + )} + +
+
+
+
+ ); +} diff --git a/src/pages/admin/AdmissionRegisterPage.tsx b/src/pages/admin/AdmissionRegisterPage.tsx new file mode 100644 index 0000000..b47dd02 --- /dev/null +++ b/src/pages/admin/AdmissionRegisterPage.tsx @@ -0,0 +1,182 @@ +import { useState } from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, CheckCircle2, XCircle, Loader2 } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { admissionService } from "@/services/admission.service"; +import { useCourses } from "@/hooks/queries"; +import { useToast } from "@/hooks/use-toast"; +import type { AdmissionRegister, AdmissionRegisterCreateRequest } from "@/types/admission"; + +const stateBadgeVariant: Record = { + draft: "outline", + confirm: "default", + cancel: "destructive", + done: "secondary", +}; + +export default function AdmissionRegisterPage() { + const { toast } = useToast(); + const qc = useQueryClient(); + const [showCreate, setShowCreate] = useState(false); + + const { data: registersData, isLoading } = useQuery({ + queryKey: ["admission-registers", "list"], + queryFn: () => admissionService.listRegisters(), + }); + const registers = registersData?.items ?? []; + + const { data: coursesData } = useCourses(); + const courses = coursesData?.items ?? []; + + const [form, setForm] = useState({ + name: "", + course_id: 0, + start_date: "", + end_date: "", + min_count: undefined, + max_count: undefined, + }); + + const createMutation = useMutation({ + mutationFn: (data: AdmissionRegisterCreateRequest) => admissionService.createRegister(data), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["admission-registers"] }); + toast({ title: "Register created" }); + setShowCreate(false); + setForm({ name: "", course_id: 0, start_date: "", end_date: "" }); + }, + onError: () => toast({ title: "Error", description: "Failed to create register", variant: "destructive" }), + }); + + const confirmMutation = useMutation({ + mutationFn: (id: number) => admissionService.confirmRegister(id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ["admission-registers"] }); toast({ title: "Register confirmed" }); }, + onError: () => toast({ title: "Error", description: "Failed to confirm", variant: "destructive" }), + }); + + const closeMutation = useMutation({ + mutationFn: (id: number) => admissionService.closeRegister(id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ["admission-registers"] }); toast({ title: "Register closed" }); }, + onError: () => toast({ title: "Error", description: "Failed to close", variant: "destructive" }), + }); + + if (isLoading) return
; + + return ( +
+
+
+

Admission Registers

+

Manage admission registers and their status.

+
+ + + + + + Create Admission Register +
+
+ + setForm({ ...form, name: e.target.value })} /> +
+
+ + +
+
+
+ + setForm({ ...form, start_date: e.target.value })} /> +
+
+ + setForm({ ...form, end_date: e.target.value })} /> +
+
+
+
+ + setForm({ ...form, min_count: e.target.value ? Number(e.target.value) : undefined })} /> +
+
+ + setForm({ ...form, max_count: e.target.value ? Number(e.target.value) : undefined })} /> +
+
+ +
+
+
+
+ + + + + + + Name + Course + Dates + Capacity + Applications + State + Actions + + + + {registers.map((reg: AdmissionRegister) => ( + + {reg.name} + {reg.course_name} + {reg.start_date} — {reg.end_date} + {reg.min_count}–{reg.max_count} + {reg.application_count} + + {reg.state} + + +
+ {reg.state === "draft" && ( + + )} + {reg.state === "confirm" && ( + + )} +
+
+
+ ))} + {registers.length === 0 && ( + + No admission registers found. + + )} +
+
+
+
+
+ ); +} diff --git a/src/pages/admin/DepartmentManager.tsx b/src/pages/admin/DepartmentManager.tsx new file mode 100644 index 0000000..112b8d9 --- /dev/null +++ b/src/pages/admin/DepartmentManager.tsx @@ -0,0 +1,154 @@ +import { useState } from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, Pencil, Trash2, Loader2 } from "lucide-react"; +import { + useDepartments, + useCreateDepartment, + useUpdateDepartment, + useDeleteDepartment, +} from "@/hooks/queries/useAcademic"; +import { useToast } from "@/hooks/use-toast"; +import type { Department, DepartmentCreateRequest } from "@/types/academic"; + +export default function DepartmentManager() { + const { toast } = useToast(); + const { data: deptsData, isLoading } = useDepartments(); + const depts = deptsData?.items ?? []; + const createDept = useCreateDepartment(); + const updateDept = useUpdateDepartment(); + const deleteDept = useDeleteDepartment(); + const [showDialog, setShowDialog] = useState(false); + const [editing, setEditing] = useState(null); + const [form, setForm] = useState({ name: "", code: "" }); + + const openCreate = () => { + setEditing(null); + setForm({ name: "", code: "" }); + setShowDialog(true); + }; + + const openEdit = (dept: Department) => { + setEditing(dept); + setForm({ name: dept.name, code: dept.code, parent_id: dept.parent_id }); + setShowDialog(true); + }; + + const handleSave = () => { + if (editing) { + updateDept.mutate({ id: editing.id, data: form }, { + onSuccess: () => { toast({ title: "Department updated" }); setShowDialog(false); }, + onError: () => toast({ title: "Error", description: "Failed to update", variant: "destructive" }), + }); + } else { + createDept.mutate(form, { + onSuccess: () => { toast({ title: "Department created" }); setShowDialog(false); setForm({ name: "", code: "" }); }, + onError: () => toast({ title: "Error", description: "Failed to create", variant: "destructive" }), + }); + } + }; + + const handleDelete = (id: number) => { + deleteDept.mutate(id, { + onSuccess: () => toast({ title: "Department deleted" }), + onError: () => toast({ title: "Error", description: "Failed to delete", variant: "destructive" }), + }); + }; + + const isSaving = createDept.isPending || updateDept.isPending; + + if (isLoading) return
; + + return ( +
+
+
+

Departments

+

Manage departments and their hierarchy.

+
+ + + + + + {editing ? "Edit Department" : "Create Department"} +
+
+ + setForm({ ...form, name: e.target.value })} /> +
+
+ + setForm({ ...form, code: e.target.value })} /> +
+
+ + +
+ +
+
+
+
+ + + + + + + Name + Code + Parent + Courses + Faculty + Actions + + + + {depts.map((dept: Department) => ( + + {dept.name} + {dept.code} + {dept.parent_name ?? "—"} + {dept.course_count} + {dept.faculty_count} + +
+ + +
+
+
+ ))} + {depts.length === 0 && ( + + No departments found. + + )} +
+
+
+
+
+ ); +} diff --git a/src/pages/admin/InstitutionalExamSessions.tsx b/src/pages/admin/InstitutionalExamSessions.tsx new file mode 100644 index 0000000..9fb7b9c --- /dev/null +++ b/src/pages/admin/InstitutionalExamSessions.tsx @@ -0,0 +1,274 @@ +import { useState } from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, ChevronRight, CalendarCheck, CheckCircle2, Loader2 } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { institutionalExamService } from "@/services/institutional-exam.service"; +import { useCourses, useBatches } from "@/hooks/queries"; +import { useToast } from "@/hooks/use-toast"; +import type { InstitutionalExamSession, InstitutionalExam } from "@/types/institutional-exam"; + +const stateBadgeVariant: Record = { + draft: "outline", + schedule: "secondary", + held: "default", + cancel: "destructive", + done: "default", +}; + +export default function InstitutionalExamSessions() { + const { toast } = useToast(); + const qc = useQueryClient(); + const [showCreate, setShowCreate] = useState(false); + const [expandedId, setExpandedId] = useState(null); + + const { data: sessionsData, isLoading } = useQuery({ + queryKey: ["inst-exam-sessions", "list"], + queryFn: () => institutionalExamService.listSessions(), + }); + const sessions = sessionsData?.items ?? []; + + const { data: expandedSession } = useQuery({ + queryKey: ["inst-exam-sessions", expandedId], + queryFn: () => institutionalExamService.getSession(expandedId!), + enabled: !!expandedId, + }); + + const { data: examTypesData } = useQuery({ + queryKey: ["exam-types", "list"], + queryFn: () => institutionalExamService.listExamTypes(), + }); + const examTypes = examTypesData?.items ?? []; + + const { data: coursesData } = useCourses(); + const courses = coursesData?.items ?? []; + const { data: batchesData } = useBatches(); + const batches = batchesData?.items ?? []; + + const [form, setForm] = useState({ + name: "", + course_id: 0, + batch_id: 0, + start_date: "", + end_date: "", + exam_type_id: 0, + evaluation_type: "normal" as "normal" | "grade", + }); + + const createMutation = useMutation({ + mutationFn: () => institutionalExamService.createSession(form as Parameters[0]), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["inst-exam-sessions"] }); + toast({ title: "Exam session created" }); + setShowCreate(false); + setForm({ name: "", course_id: 0, batch_id: 0, start_date: "", end_date: "", exam_type_id: 0, evaluation_type: "normal" }); + }, + onError: () => toast({ title: "Error", description: "Failed to create session", variant: "destructive" }), + }); + + const scheduleMutation = useMutation({ + mutationFn: (id: number) => institutionalExamService.scheduleSession(id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ["inst-exam-sessions"] }); toast({ title: "Session scheduled" }); }, + onError: () => toast({ title: "Error", description: "Failed to schedule", variant: "destructive" }), + }); + + const doneMutation = useMutation({ + mutationFn: (id: number) => institutionalExamService.markSessionDone(id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ["inst-exam-sessions"] }); toast({ title: "Session marked as done" }); }, + onError: () => toast({ title: "Error", description: "Failed to update", variant: "destructive" }), + }); + + if (isLoading) return
; + + return ( +
+
+
+

Exam Sessions

+

Manage institutional exam sessions and their exams.

+
+ + + + + + Create Exam Session +
+
+ + setForm({ ...form, name: e.target.value })} /> +
+
+
+ + +
+
+ + +
+
+
+
+ + setForm({ ...form, start_date: e.target.value })} /> +
+
+ + setForm({ ...form, end_date: e.target.value })} /> +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + + + + + + + Name + Course / Batch + Dates + Type + Exams + State + Actions + + + + {sessions.map((session: InstitutionalExamSession) => ( + setExpandedId(open ? session.id : null)}> + <> + + + + + + + {session.name} + +
+ {session.course_name} + / {session.batch_name} +
+
+ {session.start_date} — {session.end_date} + {session.exam_type_name} + {session.exam_count} + + {session.state} + + +
+ {session.state === "draft" && ( + + )} + {session.state === "schedule" && ( + + )} +
+
+
+ + + + {expandedSession && expandedId === session.id ? ( +
+

Exams in Session

+ {(expandedSession as InstitutionalExamSession & { exams?: InstitutionalExam[] }).exams?.length ? ( +
+ {((expandedSession as InstitutionalExamSession & { exams?: InstitutionalExam[] }).exams ?? []).map((exam: InstitutionalExam) => ( +
+
+ {exam.name} + {exam.subject_name} +
+
+ {exam.total_marks} marks + {exam.state} +
+
+ ))} +
+ ) : ( +

No exams in this session yet.

+ )} +
+ ) : ( +
+ )} +
+
+
+ +
+ ))} + {sessions.length === 0 && ( + + No exam sessions found. + + )} +
+
+
+
+
+ ); +} diff --git a/src/pages/admin/MarksheetManager.tsx b/src/pages/admin/MarksheetManager.tsx new file mode 100644 index 0000000..d23cd35 --- /dev/null +++ b/src/pages/admin/MarksheetManager.tsx @@ -0,0 +1,312 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Plus, FileSpreadsheet, CheckCircle2, Loader2 } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { institutionalExamService } from "@/services/institutional-exam.service"; +import { useToast } from "@/hooks/use-toast"; +import type { ResultTemplate, Marksheet, MarksheetLine, GradeConfig } from "@/types/institutional-exam"; + +const marksheetStateBadge: Record = { + draft: "outline", + validated: "default", + cancelled: "destructive", +}; + +export default function MarksheetManager() { + const { toast } = useToast(); + const qc = useQueryClient(); + const [activeTab, setActiveTab] = useState<"templates" | "marksheets">("templates"); + const [showCreateTemplate, setShowCreateTemplate] = useState(false); + const [selectedMarksheetId, setSelectedMarksheetId] = useState(null); + + const { data: templatesData, isLoading: loadingTemplates } = useQuery({ + queryKey: ["result-templates", "list"], + queryFn: () => institutionalExamService.listResultTemplates(), + }); + const templates = templatesData?.items ?? []; + + const { data: marksheetsData, isLoading: loadingMarksheets } = useQuery({ + queryKey: ["marksheets", "list"], + queryFn: () => institutionalExamService.listMarksheets(), + enabled: activeTab === "marksheets", + }); + const marksheets = marksheetsData?.items ?? []; + + const { data: selectedMarksheet } = useQuery({ + queryKey: ["marksheets", selectedMarksheetId], + queryFn: () => institutionalExamService.getMarksheet(selectedMarksheetId!), + enabled: !!selectedMarksheetId, + }); + + const { data: sessionsData } = useQuery({ + queryKey: ["inst-exam-sessions", "list"], + queryFn: () => institutionalExamService.listSessions(), + }); + const sessions = sessionsData?.items ?? []; + + const { data: gradeConfigsData } = useQuery({ + queryKey: ["grade-configs", "list"], + queryFn: () => institutionalExamService.listGradeConfigs(), + }); + const gradeConfigs = gradeConfigsData?.items ?? []; + + const [templateForm, setTemplateForm] = useState({ + name: "", + exam_session_id: 0, + grade_ids: [] as number[], + }); + + const createTemplateMutation = useMutation({ + mutationFn: () => institutionalExamService.createResultTemplate(templateForm as Parameters[0]), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["result-templates"] }); + toast({ title: "Template created" }); + setShowCreateTemplate(false); + setTemplateForm({ name: "", exam_session_id: 0, grade_ids: [] }); + }, + onError: () => toast({ title: "Error", description: "Failed to create template", variant: "destructive" }), + }); + + const generateMutation = useMutation({ + mutationFn: (templateId: number) => institutionalExamService.generateMarksheets(templateId), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["marksheets"] }); + qc.invalidateQueries({ queryKey: ["result-templates"] }); + toast({ title: "Marksheets generated" }); + }, + onError: () => toast({ title: "Error", description: "Generation failed", variant: "destructive" }), + }); + + const validateMutation = useMutation({ + mutationFn: (id: number) => institutionalExamService.validateMarksheet(id), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["marksheets"] }); + toast({ title: "Marksheet validated" }); + }, + onError: () => toast({ title: "Error", description: "Validation failed", variant: "destructive" }), + }); + + const isLoading = activeTab === "templates" ? loadingTemplates : loadingMarksheets; + + if (isLoading) return
; + + return ( +
+
+
+

Marksheet Manager

+

Manage result templates and marksheets.

+
+
+ +
+ + +
+ + {activeTab === "templates" && ( + + + Result Templates + + + + + + Create Result Template +
+
+ + setTemplateForm({ ...templateForm, name: e.target.value })} /> +
+
+ + +
+
+ +
+ {gradeConfigs.map((gc: GradeConfig) => ( + + ))} + {gradeConfigs.length === 0 &&

No grade configs available.

} +
+
+ +
+
+
+
+ + + + + Name + Exam Session + Evaluation + Result Date + State + Actions + + + + {templates.map((tpl: ResultTemplate) => ( + + {tpl.name} + {tpl.exam_session_name} + {tpl.evaluation_type} + {tpl.result_date} + + + {tpl.state.replace("_", " ")} + + + + {tpl.state === "draft" && ( + + )} + + + ))} + {templates.length === 0 && ( + + No result templates found. + + )} + +
+
+
+ )} + + {activeTab === "marksheets" && ( +
+ + + + + + Name + Exam Session + Generated + Pass + Fail + State + Actions + + + + {marksheets.map((ms: Marksheet) => ( + setSelectedMarksheetId(selectedMarksheetId === ms.id ? null : ms.id)} + > + {ms.name} + {ms.exam_session_name} + {ms.generated_date} by {ms.generated_by_name} + {ms.total_pass} + {ms.total_failed} + + {ms.state} + + e.stopPropagation()}> + {ms.state === "draft" && ( + + )} + + + ))} + {marksheets.length === 0 && ( + + No marksheets found. + + )} + +
+
+
+ + {selectedMarksheet && ( + + + Results — {selectedMarksheet.name} + + + + + + Student + Total Marks + Percentage + Grade + Status + + + + {(selectedMarksheet.lines ?? []).map((line: MarksheetLine) => ( + + {line.student_name} + {line.total_marks} + {line.percentage.toFixed(1)}% + {line.grade ?? "—"} + + {line.status} + + + ))} + {(selectedMarksheet.lines ?? []).length === 0 && ( + + No results in this marksheet. + + )} + +
+
+
+ )} +
+ )} +
+ ); +} diff --git a/src/pages/student/SubjectRegistrationPage.tsx b/src/pages/student/SubjectRegistrationPage.tsx new file mode 100644 index 0000000..5b82f4b --- /dev/null +++ b/src/pages/student/SubjectRegistrationPage.tsx @@ -0,0 +1,145 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Loader2 } from "lucide-react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { institutionalExamService } from "@/services/institutional-exam.service"; +import { useToast } from "@/hooks/use-toast"; +import type { SubjectRegistration } from "@/types/institutional-exam"; + +const stateBadgeVariant: Record = { + draft: "outline", + confirm: "default", + reject: "destructive", + done: "secondary", +}; + +export default function SubjectRegistrationPage() { + const { toast } = useToast(); + const qc = useQueryClient(); + const [selectedSubjects, setSelectedSubjects] = useState([]); + + const { data: registrationsData, isLoading } = useQuery({ + queryKey: ["subject-registrations", "list"], + queryFn: () => institutionalExamService.listSubjectRegistrations(), + }); + const registrations = registrationsData?.items ?? []; + + const { data: available = [], isLoading: loadingAvailable } = useQuery({ + queryKey: ["subject-registrations", "available"], + queryFn: () => institutionalExamService.getAvailableSubjects(), + }); + + const createMutation = useMutation({ + mutationFn: () => institutionalExamService.createSubjectRegistration({ + subject_ids: selectedSubjects, + } as Parameters[0]), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["subject-registrations"] }); + toast({ title: "Registration submitted" }); + setSelectedSubjects([]); + }, + onError: () => toast({ title: "Error", description: "Registration failed", variant: "destructive" }), + }); + + const toggleSubject = (id: number) => { + setSelectedSubjects(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id]); + }; + + if (isLoading) return
; + + return ( +
+
+

Subject Registration

+

Register for subjects within your enrolled courses.

+
+ + + + Available Subjects + Select the subjects you want to register for this term. + + + {loadingAvailable ? ( +
+ ) : available.length > 0 ? ( +
+
+ {available.map((subj: SubjectRegistration) => ( + + ))} +
+ +
+ ) : ( +

No subjects available for registration.

+ )} +
+
+ + + + My Registrations + + + + + + Course + Batch + Subjects + State + + + + {registrations.map((reg: SubjectRegistration) => ( + + {reg.course_name} + {reg.batch_name} + +
+ {reg.subject_names.slice(0, 3).map((name, i) => ( + {name} + ))} + {reg.subject_names.length > 3 && +{reg.subject_names.length - 3}} +
+
+ + {reg.state} + +
+ ))} + {registrations.length === 0 && ( + + No registrations yet. + + )} +
+
+
+
+
+ ); +} diff --git a/src/services/academic.service.ts b/src/services/academic.service.ts new file mode 100644 index 0000000..9b7c858 --- /dev/null +++ b/src/services/academic.service.ts @@ -0,0 +1,64 @@ +import { api } from "@/lib/api-client"; +import type { + AcademicYear, AcademicTerm, Department, + AcademicYearCreateRequest, AcademicTermCreateRequest, DepartmentCreateRequest, +} from "@/types/academic"; +import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types"; + +export const academicService = { + async listAcademicYears(params?: PaginationParams): Promise> { + return api.get>("/academic-years", params as Record); + }, + + async getAcademicYear(id: number): Promise { + return api.get(`/academic-years/${id}`); + }, + + async createAcademicYear(data: AcademicYearCreateRequest): Promise { + return api.post("/academic-years", data); + }, + + async updateAcademicYear(id: number, data: Partial): Promise { + return api.patch(`/academic-years/${id}`, data); + }, + + async deleteAcademicYear(id: number): Promise { + return api.delete(`/academic-years/${id}`); + }, + + async generateTerms(yearId: number): Promise { + return api.post(`/academic-years/${yearId}/generate-terms`); + }, + + async listTerms(params?: PaginationParams & { year_id?: number }): Promise> { + return api.get>("/academic-terms", params as Record); + }, + + async createTerm(data: AcademicTermCreateRequest): Promise { + return api.post("/academic-terms", data); + }, + + async updateTerm(id: number, data: Partial): Promise { + return api.patch(`/academic-terms/${id}`, data); + }, + + async deleteTerm(id: number): Promise { + return api.delete(`/academic-terms/${id}`); + }, + + async listDepartments(params?: PaginationParams): Promise> { + return api.get>("/departments", params as Record); + }, + + async createDepartment(data: DepartmentCreateRequest): Promise { + return api.post("/departments", data); + }, + + async updateDepartment(id: number, data: Partial): Promise { + return api.patch(`/departments/${id}`, data); + }, + + async deleteDepartment(id: number): Promise { + return api.delete(`/departments/${id}`); + }, +}; diff --git a/src/services/admission.service.ts b/src/services/admission.service.ts new file mode 100644 index 0000000..84aca80 --- /dev/null +++ b/src/services/admission.service.ts @@ -0,0 +1,56 @@ +import { api } from "@/lib/api-client"; +import type { + AdmissionRegister, Admission, + AdmissionRegisterCreateRequest, AdmissionCreateRequest, +} from "@/types/admission"; +import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types"; + +export const admissionService = { + async listRegisters(params?: PaginationParams): Promise> { + return api.get>("/admission-registers", params as Record); + }, + + async createRegister(data: AdmissionRegisterCreateRequest): Promise { + return api.post("/admission-registers", data); + }, + + async updateRegister(id: number, data: Partial): Promise { + return api.patch(`/admission-registers/${id}`, data); + }, + + async confirmRegister(id: number): Promise { + return api.post(`/admission-registers/${id}/confirm`); + }, + + async closeRegister(id: number): Promise { + return api.post(`/admission-registers/${id}/close`); + }, + + async listAdmissions(params?: PaginationParams & { state?: string; register_id?: number; course_id?: number }): Promise> { + return api.get>("/admissions", params as Record); + }, + + async getAdmission(id: number): Promise { + return api.get(`/admissions/${id}`); + }, + + async createAdmission(data: AdmissionCreateRequest): Promise { + return api.post("/admissions", data); + }, + + async submitAdmission(id: number): Promise { + return api.post(`/admissions/${id}/submit`); + }, + + async confirmAdmission(id: number): Promise { + return api.post(`/admissions/${id}/confirm`); + }, + + async admitAdmission(id: number): Promise { + return api.post(`/admissions/${id}/admit`); + }, + + async rejectAdmission(id: number): Promise { + return api.post(`/admissions/${id}/reject`); + }, +}; diff --git a/src/services/index.ts b/src/services/index.ts index 70a4410..3b38558 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -19,3 +19,6 @@ export { resourcesService } from "./resources.service"; export { coachingService } from "./coaching.service"; export { analyticsService } from "./analytics.service"; export { lmsService } from "./lms.service"; +export { academicService } from "./academic.service"; +export { admissionService } from "./admission.service"; +export { institutionalExamService } from "./institutional-exam.service"; diff --git a/src/services/institutional-exam.service.ts b/src/services/institutional-exam.service.ts new file mode 100644 index 0000000..9297e08 --- /dev/null +++ b/src/services/institutional-exam.service.ts @@ -0,0 +1,165 @@ +import { api } from "@/lib/api-client"; +import type { + InstExamSession, InstExam, ExamType, GradeConfig, + ResultTemplate, Marksheet, CourseAssignment, AssignmentSubmission, + AssignmentType, SubjectRegistration, + InstExamSessionCreateRequest, InstExamCreateRequest, + ExamTypeCreateRequest, GradeConfigCreateRequest, + ResultTemplateCreateRequest, CourseAssignmentCreateRequest, + AssignmentTypeCreateRequest, SubjectRegistrationCreateRequest, +} from "@/types/institutional-exam"; +import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types"; + +export const institutionalExamService = { + async listSessions(params?: PaginationParams): Promise> { + return api.get>("/inst-exam-sessions", params as Record); + }, + + async getSession(id: number): Promise { + return api.get(`/inst-exam-sessions/${id}`); + }, + + async createSession(data: InstExamSessionCreateRequest): Promise { + return api.post("/inst-exam-sessions", data); + }, + + async updateSession(id: number, data: Partial): Promise { + return api.patch(`/inst-exam-sessions/${id}`, data); + }, + + async deleteSession(id: number): Promise { + return api.delete(`/inst-exam-sessions/${id}`); + }, + + async scheduleSession(id: number): Promise { + return api.post(`/inst-exam-sessions/${id}/schedule`); + }, + + async markSessionDone(id: number): Promise { + return api.post(`/inst-exam-sessions/${id}/done`); + }, + + async createExam(data: InstExamCreateRequest): Promise { + return api.post("/inst-exams", data); + }, + + async updateExam(id: number, data: Partial): Promise { + return api.patch(`/inst-exams/${id}`, data); + }, + + async addAttendees(examId: number, data: { student_ids: number[] }): Promise { + return api.post(`/inst-exams/${examId}/attendees`, data); + }, + + async enterMarks(examId: number, data: { marks: { student_id: number; score: number }[] }): Promise { + return api.patch(`/inst-exams/${examId}/marks`, data); + }, + + async listExamTypes(params?: PaginationParams): Promise> { + return api.get>("/exam-types", params as Record); + }, + + async createExamType(data: ExamTypeCreateRequest): Promise { + return api.post("/exam-types", data); + }, + + async listGradeConfigs(params?: PaginationParams): Promise> { + return api.get>("/grade-config", params as Record); + }, + + async createGradeConfig(data: GradeConfigCreateRequest): Promise { + return api.post("/grade-config", data); + }, + + async listResultTemplates(params?: PaginationParams): Promise> { + return api.get>("/result-templates", params as Record); + }, + + async createResultTemplate(data: ResultTemplateCreateRequest): Promise { + return api.post("/result-templates", data); + }, + + async generateMarksheets(templateId: number): Promise { + return api.post(`/result-templates/${templateId}/generate`); + }, + + async listMarksheets(params?: PaginationParams): Promise> { + return api.get>("/marksheets", params as Record); + }, + + async getMarksheet(id: number): Promise { + return api.get(`/marksheets/${id}`); + }, + + async validateMarksheet(id: number): Promise { + return api.post(`/marksheets/${id}/validate`); + }, + + async listCourseAssignments(params?: PaginationParams & { course_id?: number; batch_id?: number; state?: string }): Promise> { + return api.get>("/course-assignments", params as Record); + }, + + async createCourseAssignment(data: CourseAssignmentCreateRequest): Promise { + return api.post("/course-assignments", data); + }, + + async getCourseAssignment(id: number): Promise { + return api.get(`/course-assignments/${id}`); + }, + + async updateCourseAssignment(id: number, data: Partial): Promise { + return api.patch(`/course-assignments/${id}`, data); + }, + + async publishCourseAssignment(id: number): Promise { + return api.post(`/course-assignments/${id}/publish`); + }, + + async finishCourseAssignment(id: number): Promise { + return api.post(`/course-assignments/${id}/finish`); + }, + + async listSubmissions(assignmentId: number, params?: PaginationParams): Promise> { + return api.get>(`/course-assignments/${assignmentId}/submissions`, params as Record); + }, + + async submitAssignment(assignmentId: number, file: File): Promise { + return api.upload(`/course-assignments/${assignmentId}/submit`, file); + }, + + async gradeSubmission(submissionId: number, data: Partial): Promise { + return api.patch(`/submissions/${submissionId}`, data); + }, + + async listAssignmentTypes(params?: PaginationParams): Promise> { + return api.get>("/assignment-types", params as Record); + }, + + async createAssignmentType(data: AssignmentTypeCreateRequest): Promise { + return api.post("/assignment-types", data); + }, + + async listSubjectRegistrations(params?: PaginationParams & { student_id?: number; course_id?: number; state?: string }): Promise> { + return api.get>("/subject-registrations", params as Record); + }, + + async createSubjectRegistration(data: SubjectRegistrationCreateRequest): Promise { + return api.post("/subject-registrations", data); + }, + + async updateSubjectRegistration(id: number, data: Partial): Promise { + return api.patch(`/subject-registrations/${id}`, data); + }, + + async confirmSubjectRegistration(id: number): Promise { + return api.post(`/subject-registrations/${id}/confirm`); + }, + + async rejectSubjectRegistration(id: number): Promise { + return api.post(`/subject-registrations/${id}/reject`); + }, + + async getAvailableSubjects(params?: { student_id?: number; course_id?: number }): Promise { + return api.get("/subject-registrations/available", params as Record); + }, +}; diff --git a/src/types/academic.ts b/src/types/academic.ts new file mode 100644 index 0000000..356bed2 --- /dev/null +++ b/src/types/academic.ts @@ -0,0 +1,50 @@ +export interface AcademicYear { + id: number; + name: string; + start_date: string; + end_date: string; + term_structure: "two_sem" | "two_sem_qua" | "three_sem" | "four_Quarter" | "final_year" | "others"; + terms: AcademicTerm[]; +} + +export interface AcademicTerm { + id: number; + name: string; + term_start_date: string; + term_end_date: string; + academic_year_id: number; + academic_year_name: string; + parent_term_id?: number; + parent_term_name?: string; +} + +export interface Department { + id: number; + name: string; + code: string; + parent_id?: number; + parent_name?: string; + course_count: number; + faculty_count: number; +} + +export interface AcademicYearCreateRequest { + name: string; + start_date: string; + end_date: string; + term_structure: AcademicYear["term_structure"]; +} + +export interface AcademicTermCreateRequest { + name: string; + term_start_date: string; + term_end_date: string; + academic_year_id: number; + parent_term_id?: number; +} + +export interface DepartmentCreateRequest { + name: string; + code: string; + parent_id?: number; +} diff --git a/src/types/admission.ts b/src/types/admission.ts new file mode 100644 index 0000000..100bcae --- /dev/null +++ b/src/types/admission.ts @@ -0,0 +1,63 @@ +export type AdmissionRegisterState = "draft" | "confirm" | "cancel" | "done"; + +export interface AdmissionRegister { + id: number; + name: string; + course_id: number; + course_name: string; + start_date: string; + end_date: string; + min_count: number; + max_count: number; + application_count: number; + state: AdmissionRegisterState; +} + +export type AdmissionState = "draft" | "submit" | "confirm" | "admission" | "reject" | "cancel" | "done"; + +export interface Admission { + id: number; + name: string; + application_number: string; + register_id: number; + register_name: string; + course_id: number; + course_name: string; + batch_id?: number; + batch_name?: string; + first_name: string; + middle_name?: string; + last_name: string; + email: string; + phone?: string; + birth_date?: string; + gender: "m" | "f" | "o"; + nationality_id?: number; + nationality_name?: string; + state: AdmissionState; + fees: number; + image?: string; + student_id?: number; + created_at: string; +} + +export interface AdmissionCreateRequest { + register_id: number; + first_name: string; + middle_name?: string; + last_name: string; + email: string; + phone?: string; + birth_date?: string; + gender: "m" | "f" | "o"; + nationality_id?: number; +} + +export interface AdmissionRegisterCreateRequest { + name: string; + course_id: number; + start_date: string; + end_date: string; + min_count?: number; + max_count?: number; +} diff --git a/src/types/index.ts b/src/types/index.ts index 29098ed..0133c07 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,3 +12,6 @@ export * from "./taxonomy"; export * from "./adaptive"; export * from "./lms"; export * from "./ai"; +export * from "./academic"; +export * from "./admission"; +export * from "./institutional-exam"; diff --git a/src/types/institutional-exam.ts b/src/types/institutional-exam.ts new file mode 100644 index 0000000..24197c0 --- /dev/null +++ b/src/types/institutional-exam.ts @@ -0,0 +1,176 @@ +export type ExamSessionState = "draft" | "schedule" | "held" | "cancel" | "done"; +export type InstitutionalExamState = "draft" | "schedule" | "held" | "result_updated" | "cancel" | "done"; +export type MarksheetState = "draft" | "validated" | "cancelled"; +export type SubmissionState = "draft" | "submit" | "accept" | "reject" | "change_req"; + +export interface ExamType { + id: number; + name: string; +} + +export interface ExamRoom { + id: number; + name: string; + classroom_id?: number; + classroom_name?: string; + capacity: number; +} + +export interface InstitutionalExamSession { + id: number; + name: string; + course_id: number; + course_name: string; + batch_id: number; + batch_name: string; + exam_code: string; + start_date: string; + end_date: string; + exam_type_id: number; + exam_type_name: string; + evaluation_type: "normal" | "grade"; + venue?: string; + state: ExamSessionState; + exam_count: number; +} + +export interface InstitutionalExam { + id: number; + name: string; + session_id: number; + subject_id: number; + subject_name: string; + exam_code: string; + start_time: string; + end_time: string; + total_marks: number; + min_marks: number; + state: InstitutionalExamState; + responsible_names: string[]; + attendee_count: number; +} + +export interface ExamAttendee { + id: number; + exam_id: number; + student_id: number; + student_name: string; + room_id?: number; + room_name?: string; + marks: number; + status: "pass" | "fail"; +} + +export interface GradeConfig { + id: number; + min_per: number; + max_per: number; + result: string; +} + +export interface ResultTemplate { + id: number; + name: string; + exam_session_id: number; + exam_session_name: string; + evaluation_type: "normal" | "grade"; + result_date: string; + grade_ids: number[]; + state: "draft" | "result_generated"; +} + +export interface Marksheet { + id: number; + name: string; + exam_session_id: number; + exam_session_name: string; + result_template_id: number; + generated_date: string; + generated_by_name: string; + state: MarksheetState; + total_pass: number; + total_failed: number; + lines: MarksheetLine[]; +} + +export interface MarksheetLine { + id: number; + student_id: number; + student_name: string; + total_marks: number; + percentage: number; + grade?: string; + status: "pass" | "fail"; + results: ResultLine[]; +} + +export interface ResultLine { + id: number; + exam_id: number; + exam_name: string; + subject_name: string; + marks: number; + grade?: string; + status: "pass" | "fail"; +} + +export interface CourseAssignmentType { + id: number; + name: string; +} + +export interface CourseAssignment { + id: number; + name: string; + course_id: number; + course_name: string; + subject_id?: number; + subject_name?: string; + batch_id: number; + batch_name: string; + faculty_id: number; + faculty_name: string; + description: string; + marks: number; + point: number; + assignment_type_id: number; + assignment_type_name: string; + issued_date: string; + submission_date: string; + state: "draft" | "publish" | "finish" | "cancel"; + allocation_count: number; + submission_count: number; + reviewer_id?: number; + reviewer_name?: string; +} + +export interface AssignmentSubmission { + id: number; + assignment_id: number; + student_id: number; + student_name: string; + description?: string; + submission_date?: string; + attachment_ids: number[]; + attachment_names: string[]; + marks: number; + state: SubmissionState; + faculty_id?: number; + faculty_name?: string; + note?: string; +} + +export interface SubjectRegistration { + id: number; + student_id: number; + student_name: string; + course_id: number; + course_name: string; + batch_id: number; + batch_name: string; + subject_ids: number[]; + subject_names: string[]; + min_unit_load: number; + max_unit_load: number; + state: "draft" | "confirm" | "reject" | "done"; +}