- Restructure: move backend from new_project/ to backend/ - Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks) - Add docs/ with SRS specs, user stories, and workflow documentation - Update .gitignore for new directory layout Workflows implemented: WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration, WF4 General English Exam, WF5 Course Generation, WF6 Entity Student Onboarding, AI Course Generation, Adaptive Learning Engine UI, White-Label Branding, Score Release Made-with: Cursor
226 lines
8.5 KiB
TypeScript
226 lines
8.5 KiB
TypeScript
import { api } from "@/lib/api-client";
|
|
import { asPaginated, asRecordData } from "@/lib/odoo-api";
|
|
import type { User } from "@/types/auth";
|
|
import type {
|
|
Course,
|
|
Batch,
|
|
TimetableSession,
|
|
AttendanceRecord,
|
|
GradeRecord,
|
|
CourseCreateRequest,
|
|
PaginatedResponse,
|
|
PaginationParams,
|
|
ApiSuccessResponse,
|
|
LmsStudentRecord,
|
|
LmsTeacherRecord,
|
|
LmsStudentCreateRequest,
|
|
LmsTeacherCreateRequest,
|
|
} from "@/types";
|
|
|
|
function mapCourseRaw(r: Record<string, unknown>): Course {
|
|
const status = (r.status as Course["status"]) || "draft";
|
|
const enrolled = Number(r.student_count ?? 0);
|
|
const maxCap = Number(r.max_capacity ?? 0);
|
|
return {
|
|
id: r.id as number,
|
|
title: (r.title as string) || (r.name as string) || "",
|
|
code: (r.code as string) || "",
|
|
subject_id: Array.isArray(r.subject_ids) && (r.subject_ids as number[]).length ? (r.subject_ids as number[])[0] : undefined,
|
|
subject_name: Array.isArray(r.subject_names) ? String((r.subject_names as string[])[0] || "") : "",
|
|
instructor_id: 0,
|
|
instructor_name: "",
|
|
description: (r.description as string) || "",
|
|
level: "Beginner",
|
|
modules: [],
|
|
enrolled,
|
|
max_capacity: maxCap,
|
|
status,
|
|
start_date: "",
|
|
end_date: "",
|
|
};
|
|
}
|
|
|
|
function mapBatchRaw(r: Record<string, unknown>): Batch {
|
|
const st = String(r.status || "draft");
|
|
const statusMap: Record<string, Batch["status"]> = {
|
|
draft: "upcoming",
|
|
active: "active",
|
|
completed: "completed",
|
|
};
|
|
return {
|
|
id: r.id as number,
|
|
name: String(r.name || ""),
|
|
course_id: Number(r.course_id || 0),
|
|
course_name: String(r.course_name || ""),
|
|
teacher_id: 0,
|
|
teacher_name: "",
|
|
student_ids: [],
|
|
student_count: 0,
|
|
start_date: String(r.start_date || ""),
|
|
end_date: String(r.end_date || ""),
|
|
schedule: "",
|
|
status: statusMap[st] || "upcoming",
|
|
capacity: Number(r.max_students ?? 0),
|
|
};
|
|
}
|
|
|
|
/** Map OpenEduCat student for screens that still expect `User` (admin tables). */
|
|
export function lmsStudentToUser(s: LmsStudentRecord): User {
|
|
const active = s.status === "active";
|
|
return {
|
|
id: s.id,
|
|
name: s.name,
|
|
email: s.email || "",
|
|
login: s.email || "",
|
|
user_type: "student",
|
|
phone: s.phone || undefined,
|
|
gender: s.gender || undefined,
|
|
student_id: String(s.id),
|
|
is_verified: active,
|
|
entities:
|
|
s.batch_id != null
|
|
? [{ id: s.batch_id, name: s.batch_name || "Batch" }]
|
|
: [],
|
|
classrooms: [],
|
|
};
|
|
}
|
|
|
|
export function lmsTeacherToUser(t: LmsTeacherRecord): User {
|
|
return {
|
|
id: t.id,
|
|
name: t.name,
|
|
email: t.email || "",
|
|
login: t.email || "",
|
|
user_type: "teacher",
|
|
phone: t.phone || undefined,
|
|
gender: t.gender || undefined,
|
|
is_verified: true,
|
|
entities: t.department_id ? [{ id: t.department_id, name: t.department_name || "Department" }] : [],
|
|
classrooms: [],
|
|
};
|
|
}
|
|
|
|
export const lmsService = {
|
|
async listCourses(params?: PaginationParams & { status?: string }): Promise<PaginatedResponse<Course>> {
|
|
const raw = await api.get<unknown>("/courses", params as Record<string, string | number | boolean | undefined>);
|
|
const p = asPaginated<Record<string, unknown>>(raw);
|
|
return { ...p, items: p.items.map(mapCourseRaw) };
|
|
},
|
|
|
|
async getCourse(id: number): Promise<Course> {
|
|
const raw = await api.get<unknown>(`/courses/${id}`);
|
|
return mapCourseRaw(asRecordData<Record<string, unknown>>(raw));
|
|
},
|
|
|
|
async createCourse(data: CourseCreateRequest): Promise<Course> {
|
|
const raw = await api.post<unknown>("/courses", {
|
|
name: data.title,
|
|
code: data.code,
|
|
description: data.description,
|
|
max_capacity: data.max_capacity,
|
|
status: "draft",
|
|
});
|
|
return mapCourseRaw(asRecordData<Record<string, unknown>>(raw));
|
|
},
|
|
|
|
async updateCourse(id: number, data: Partial<CourseCreateRequest>): Promise<Course> {
|
|
const body: Record<string, unknown> = {};
|
|
if (data.title != null) body.name = data.title;
|
|
if (data.code != null) body.code = data.code;
|
|
if (data.description != null) body.description = data.description;
|
|
if (data.max_capacity != null) body.max_capacity = data.max_capacity;
|
|
const raw = await api.patch<unknown>(`/courses/${id}`, body);
|
|
return mapCourseRaw(asRecordData<Record<string, unknown>>(raw));
|
|
},
|
|
|
|
async deleteCourse(id: number): Promise<ApiSuccessResponse> {
|
|
return api.delete<ApiSuccessResponse>(`/courses/${id}`);
|
|
},
|
|
|
|
async aiGenerateCourse(data: { title: string; subject_id?: number; level?: string }): Promise<{ outline: unknown }> {
|
|
return api.post("/courses/ai-generate", data);
|
|
},
|
|
|
|
async listBatches(params?: PaginationParams): Promise<PaginatedResponse<Batch>> {
|
|
const raw = await api.get<unknown>("/batches", params as Record<string, string | number | boolean | undefined>);
|
|
const p = asPaginated<Record<string, unknown>>(raw);
|
|
return { ...p, items: p.items.map(mapBatchRaw) };
|
|
},
|
|
|
|
async getBatch(id: number): Promise<Batch> {
|
|
const raw = await api.get<unknown>(`/batches/${id}`);
|
|
return mapBatchRaw(asRecordData<Record<string, unknown>>(raw));
|
|
},
|
|
|
|
async createBatch(data: Partial<Batch>): Promise<Batch> {
|
|
const raw = await api.post<unknown>("/batches", data);
|
|
return mapBatchRaw(asRecordData<Record<string, unknown>>(raw));
|
|
},
|
|
|
|
async updateBatch(id: number, data: Partial<Batch>): Promise<Batch> {
|
|
const raw = await api.patch<unknown>(`/batches/${id}`, data);
|
|
return mapBatchRaw(asRecordData<Record<string, unknown>>(raw));
|
|
},
|
|
|
|
async deleteBatch(id: number): Promise<ApiSuccessResponse> {
|
|
return api.delete<ApiSuccessResponse>(`/batches/${id}`);
|
|
},
|
|
|
|
async listStudents(params?: PaginationParams & { search?: string; batch_id?: number }): Promise<PaginatedResponse<LmsStudentRecord>> {
|
|
const raw = await api.get<unknown>("/students", params as Record<string, string | number | boolean | undefined>);
|
|
return asPaginated<LmsStudentRecord>(raw);
|
|
},
|
|
|
|
async getStudent(id: number): Promise<LmsStudentRecord> {
|
|
const raw = await api.get<unknown>(`/students/${id}`);
|
|
return asRecordData<LmsStudentRecord>(raw);
|
|
},
|
|
|
|
async createStudent(data: LmsStudentCreateRequest): Promise<LmsStudentRecord> {
|
|
const raw = await api.post<unknown>("/students", data);
|
|
return asRecordData<LmsStudentRecord>(raw);
|
|
},
|
|
|
|
async updateStudent(id: number, data: Partial<LmsStudentCreateRequest & { status?: string }>): Promise<LmsStudentRecord> {
|
|
const raw = await api.patch<unknown>(`/students/${id}`, data);
|
|
return asRecordData<LmsStudentRecord>(raw);
|
|
},
|
|
|
|
async listTeachers(params?: PaginationParams & { search?: string }): Promise<PaginatedResponse<LmsTeacherRecord>> {
|
|
const raw = await api.get<unknown>("/teachers", params as Record<string, string | number | boolean | undefined>);
|
|
return asPaginated<LmsTeacherRecord>(raw);
|
|
},
|
|
|
|
async createTeacher(data: LmsTeacherCreateRequest): Promise<LmsTeacherRecord> {
|
|
const raw = await api.post<unknown>("/teachers", data);
|
|
return asRecordData<LmsTeacherRecord>(raw);
|
|
},
|
|
|
|
async getTimetable(params?: { course_id?: number; teacher_id?: number; batch_id?: number }): Promise<TimetableSession[]> {
|
|
const raw = await api.get<{ data: TimetableSession[] } | TimetableSession[]>("/timetable", params as Record<string, string | number | boolean | undefined>);
|
|
return Array.isArray(raw) ? raw : (raw as { data: TimetableSession[] }).data ?? [];
|
|
},
|
|
|
|
async createTimetableSession(data: Partial<TimetableSession>): Promise<TimetableSession> {
|
|
return api.post<TimetableSession>("/timetable", data);
|
|
},
|
|
|
|
async deleteTimetableSession(id: number): Promise<ApiSuccessResponse> {
|
|
return api.delete<ApiSuccessResponse>(`/timetable/${id}`);
|
|
},
|
|
|
|
async getAttendance(params?: { course_id?: number; student_id?: number; date?: string }): Promise<AttendanceRecord[]> {
|
|
const raw = await api.get<{ data: AttendanceRecord[] } | AttendanceRecord[]>("/attendance", params as Record<string, string | number | boolean | undefined>);
|
|
return Array.isArray(raw) ? raw : (raw as { data: AttendanceRecord[] }).data ?? [];
|
|
},
|
|
|
|
async recordAttendance(data: { course_id: number; date: string; records: { student_id: number; status: string }[] }): Promise<ApiSuccessResponse> {
|
|
return api.post<ApiSuccessResponse>("/attendance", data);
|
|
},
|
|
|
|
async getGrades(params?: { course_id?: number; student_id?: number }): Promise<GradeRecord[]> {
|
|
const raw = await api.get<{ data: GradeRecord[] } | GradeRecord[]>("/grades", params as Record<string, string | number | boolean | undefined>);
|
|
return Array.isArray(raw) ? raw : (raw as { data: GradeRecord[] }).data ?? [];
|
|
},
|
|
};
|