Files
full_encoach_platform/frontend/src/services/classrooms.service.ts
Yamen Ahmad 3b62075d7e feat(platform): course-plan student visibility, multi-voice TTS, OCR sources, branches
Backend (encoach_ai_course):
- workbook_attempt model + scoring + REST endpoints for student attempts
- dialogue_parser splits scripts by speaker, classifies gender, strips labels
- media_service: multi-voice TTS via Polly/ElevenLabs, ffmpeg concatenation,
  manual media upload endpoint (audio/image/video) with size validation
- source_indexer: OCR fallback (pytesseract + pdf2image) for scanned PDFs,
  page-streaming to stay under memory limit
- exercise_extractor + rag_context for RAG-grounded interactive workbooks
- course_plan_pipeline: v2 generator that grounds week material on indexed
  sources and persists grounded_on_json metadata
- security: access rules for new models

Backend (encoach_lms_api):
- branches model + controller (entity-scoped LMS branches)
- classroom_ext + course_ext (assignment + section workflow)
- classrooms controller: students/teachers/assign-course endpoints

Frontend:
- StudentDashboard: surface assigned AI course plans alongside enrollments;
  enrolled-courses stat now counts plans+enrollments
- InteractiveWorkbook + PlanReader components
- AdminCoursePlanDetail: media drawer with upload buttons (audio/image/video),
  hidden file inputs, upload mutation
- AdminBranches page + sidebar entry
- coursePlan/lms/classrooms services + types updated for new endpoints
- i18n: studentDash.myCoursePlans/noCoursePlans (en + ar)

Infra & docs:
- odoo.conf: bump memory limits to 4G/5G for OCR + sentence-transformers
- .gitignore: ignore *.tsbuildinfo
- docs/ASSIGNMENT_WORKFLOW.{md,pdf}
- smoke_*.py end-to-end tests for assignment workflow, entity isolation,
  course-plan RAG pipeline

Made-with: Cursor
2026-04-28 00:17:51 +04:00

128 lines
4.0 KiB
TypeScript

import { api } from "@/lib/api-client";
import { asPaginated, asRecordData } from "@/lib/odoo-api";
import type {
Classroom,
ClassroomAssignCourseRequest,
ClassroomBatchSummary,
ClassroomCourse,
ClassroomCreateRequest,
ClassroomMember,
ClassroomSection,
ClassroomStudent,
PaginatedResponse,
PaginationParams,
ApiSuccessResponse,
} from "@/types";
export interface ClassroomListParams extends PaginationParams {
entity_id?: number;
branch_id?: number;
search?: string;
active?: boolean;
}
export const classroomsService = {
async list(params?: ClassroomListParams): Promise<PaginatedResponse<Classroom>> {
const raw = await api.get<unknown>(
"/classrooms",
params as Record<string, string | number | boolean | undefined>,
);
return asPaginated<Classroom>(raw);
},
async getById(id: number): Promise<Classroom> {
const raw = await api.get<unknown>(`/classrooms/${id}`);
return asRecordData<Classroom>(raw);
},
async create(data: ClassroomCreateRequest): Promise<Classroom> {
const raw = await api.post<unknown>("/classrooms", data);
return asRecordData<Classroom>(raw);
},
async update(id: number, data: Partial<ClassroomCreateRequest>): Promise<Classroom> {
const raw = await api.patch<unknown>(`/classrooms/${id}`, data);
return asRecordData<Classroom>(raw);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/classrooms/${id}`);
},
// ---- Roster ----------------------------------------------------------
async listStudents(id: number): Promise<{ items: ClassroomStudent[]; total: number }> {
return api.get(`/classrooms/${id}/students`);
},
/** Replace or add to roster. ``mode='set'`` overwrites; ``mode='add'`` appends. */
async setStudents(
id: number,
studentIds: number[],
mode: "set" | "add" = "set",
): Promise<Classroom> {
const raw = await api.post<unknown>(`/classrooms/${id}/students`, {
student_ids: studentIds,
mode,
});
return asRecordData<Classroom>(raw);
},
async removeStudents(id: number, studentIds: number[]): Promise<Classroom> {
const raw = await api.delete<unknown>(`/classrooms/${id}/students`, {
student_ids: studentIds,
});
return asRecordData<Classroom>(raw);
},
// ---- Homeroom teachers ----------------------------------------------
async listTeachers(id: number): Promise<{ items: ClassroomMember[]; total: number }> {
return api.get(`/classrooms/${id}/teachers`);
},
async setTeachers(
id: number,
teacherIds: number[],
mode: "set" | "add" = "set",
): Promise<Classroom> {
const raw = await api.post<unknown>(`/classrooms/${id}/teachers`, {
teacher_ids: teacherIds,
mode,
});
return asRecordData<Classroom>(raw);
},
// ---- Courses + cascade ----------------------------------------------
async listCourses(id: number): Promise<{ items: ClassroomCourse[]; total: number }> {
return api.get(`/classrooms/${id}/courses`);
},
async listSections(
id: number,
params?: { course_id?: number; active?: boolean },
): Promise<{ items: ClassroomSection[]; total: number }> {
return api.get(
`/classrooms/${id}/sections`,
params as Record<string, string | number | boolean | undefined>,
);
},
/** Assign a course → server creates (or reuses) the canonical batch and
* enrolls every classroom student into it. */
async assignCourse(
id: number,
payload: ClassroomAssignCourseRequest,
): Promise<{ data: Classroom; batch: ClassroomBatchSummary }> {
return api.post(`/classrooms/${id}/assign-course`, payload);
},
async detachCourse(id: number, courseId: number): Promise<Classroom> {
const raw = await api.delete<unknown>(`/classrooms/${id}/courses/${courseId}`);
return asRecordData<Classroom>(raw);
},
// ---- Batches --------------------------------------------------------
async listBatches(id: number): Promise<{ items: ClassroomBatchSummary[]; total: number }> {
return api.get(`/classrooms/${id}/batches`);
},
};