feat: institutional + support + training admin sections (backend + frontend)
Ship three fully-wired admin areas end-to-end with APIs, seeds, tests and docs. Backend (new `encoach_lms_api` addon + existing addons): - Institutional: academic years/terms, departments, admission registers & admissions, courses/batches, lessons, fees (terms + student fees + invoicing with income-account auto-wiring), gradebook (assignments/grades), library, facilities (encoach.asset), student leave, result templates + marksheets (incl. delete-with-cascade). - Support: `encoach.ticket` model + CRUD/assignee routes; payment records derived from `op.student.fees.details` and `account.move`; platform settings backed by `encoach.code` and `ir.config_parameter` (packages + grading config). - Training: `encoach.vocab.item` + `encoach.grammar.rule` (plus progress models) with CRUD, pagination, search/level filters, and upsert-style progress endpoints. Odoo 19 compatibility: `_sql_constraints` replaced with `@api.constrains`; `ValidationError`/`UserError` mapped to HTTP 400. Frontend: - Rewire institutional admin pages (Academic Year Manager, Admissions, Courses, Lessons, Fees, Gradebook, Library, Facilities, Student Leave, Marksheets, Taxonomy, Resources) to real APIs with React Query invalidation and dialogs. - New typed services: `payments.service.ts`, `platformSettings.service.ts`, `training.service.ts`. Updated `fees/gradebook/lms/courseware/taxonomy/ resources/student-progress/generation` services + related types. - Rewrite `VocabularyPage`, `GrammarPage`, `PaymentRecordPage`, `SettingsPage`, `TicketsPage` to consume live data with search/filter/progress/CRUD flows. - New shared components: `TaxonomyCascade`, `MaterialViewer`, `teacher/TeacherLibrary`. - Favicons/branding assets and misc. UX polish across teacher/student pages. Tooling & QA: - Seeders: `seed_demo.py`, `seed_demo_data.py`, `seed_institutional.py` (idempotent, covers institutional + support + training fixtures incl. income-account wiring). - API write-flow test suites: `test_write_flows.py` (institutional), `test_support_flows.py` (support), `test_training_flows.py` (training), `test_ai_full.py`. All suites pass end-to-end. - Docs: add `docs/PROJECT_SUMMARY.md` with per-section scope, artifacts and QA. - `.gitignore`: ignore `pgdata_bak_*/`, `frontend/.vite/`, `frontend/dist/`, `frontend/node_modules/`. Made-with: Cursor
This commit is contained in:
@@ -51,6 +51,18 @@ export const coursewareService = {
|
||||
return api.get<ChapterProgress>(`/chapters/${id}/progress`);
|
||||
},
|
||||
|
||||
async getCourseCompletion(courseId: number): Promise<{
|
||||
course_id: number;
|
||||
status: string;
|
||||
chapters_total: number;
|
||||
chapters_completed: number;
|
||||
progress_percent: number;
|
||||
completed_at?: string | null;
|
||||
post_test_available: boolean;
|
||||
}> {
|
||||
return api.get(`/courses/${courseId}/completion`);
|
||||
},
|
||||
|
||||
async markChapterComplete(id: number): Promise<ApiSuccessResponse> {
|
||||
return api.post<ApiSuccessResponse>(`/chapters/${id}/progress/complete`);
|
||||
},
|
||||
@@ -67,6 +79,13 @@ export const coursewareService = {
|
||||
return api.patch<ChapterMaterial>(`/materials/${id}`, data);
|
||||
},
|
||||
|
||||
async createMaterialFromResource(chapterId: number, resourceId: number, name?: string): Promise<ChapterMaterial> {
|
||||
return api.post<ChapterMaterial>(`/chapters/${chapterId}/materials/from-resource`, {
|
||||
resource_id: resourceId,
|
||||
...(name ? { name } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async deleteMaterial(id: number): Promise<ApiSuccessResponse> {
|
||||
return api.delete<ApiSuccessResponse>(`/materials/${id}`);
|
||||
},
|
||||
@@ -87,6 +106,27 @@ export const coursewareService = {
|
||||
return api.patch<ApiSuccessResponse>(`/chapters/${chapterId}/materials/reorder`, body);
|
||||
},
|
||||
|
||||
async searchMaterials(params?: { search?: string; type?: string; course_id?: number; limit?: number; offset?: number }): Promise<{
|
||||
items: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
chapter_id: number;
|
||||
type: string;
|
||||
file_url: string | null;
|
||||
url: string | null;
|
||||
description: string | null;
|
||||
sequence: number;
|
||||
allow_download: boolean;
|
||||
course_id: number;
|
||||
course_name: string;
|
||||
chapter_name: string;
|
||||
source: string;
|
||||
}>;
|
||||
total: number;
|
||||
}> {
|
||||
return api.get("/materials/search", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
|
||||
async generateOutline(data: WorkbenchGenerateRequest): Promise<WorkbenchGeneratedOutline> {
|
||||
return api.post<WorkbenchGeneratedOutline>("/workbench/generate-outline", data);
|
||||
},
|
||||
|
||||
@@ -2,20 +2,51 @@ import { api } from "@/lib/api-client";
|
||||
import type { FeesPlan, StudentFeesDetail, FeesTerms } from "@/types/fees";
|
||||
import type { PaginatedResponse, PaginationParams } from "@/types";
|
||||
|
||||
export interface FeesPlanFilters extends PaginationParams {
|
||||
q?: string;
|
||||
state?: string;
|
||||
payment_state?: string;
|
||||
student_id?: number;
|
||||
}
|
||||
|
||||
export interface RegisterPaymentBody {
|
||||
amount?: number;
|
||||
journal_id?: number;
|
||||
payment_date?: string;
|
||||
memo?: string;
|
||||
}
|
||||
|
||||
export const feesService = {
|
||||
async listFeesPlans(params?: PaginationParams): Promise<PaginatedResponse<FeesPlan>> {
|
||||
return api.get<PaginatedResponse<FeesPlan>>("/fees-plans", params as Record<string, string | number | boolean | undefined>);
|
||||
async listFeesPlans(params?: FeesPlanFilters): Promise<PaginatedResponse<FeesPlan>> {
|
||||
return api.get<PaginatedResponse<FeesPlan>>(
|
||||
"/fees-plans",
|
||||
params as Record<string, string | number | boolean | undefined>,
|
||||
);
|
||||
},
|
||||
|
||||
async getFeesPlan(id: number): Promise<FeesPlan> {
|
||||
return api.get<FeesPlan>(`/fees-plans/${id}`);
|
||||
},
|
||||
|
||||
async listStudentFees(params?: PaginationParams): Promise<PaginatedResponse<StudentFeesDetail>> {
|
||||
return api.get<PaginatedResponse<StudentFeesDetail>>("/student-fees", params as Record<string, string | number | boolean | undefined>);
|
||||
async createInvoice(id: number): Promise<FeesPlan> {
|
||||
return api.post<FeesPlan>(`/fees-plans/${id}/create-invoice`, {});
|
||||
},
|
||||
|
||||
async registerPayment(id: number, body: RegisterPaymentBody = {}): Promise<FeesPlan> {
|
||||
return api.post<FeesPlan>(`/fees-plans/${id}/register-payment`, body);
|
||||
},
|
||||
|
||||
async listStudentFees(params?: FeesPlanFilters): Promise<PaginatedResponse<StudentFeesDetail>> {
|
||||
return api.get<PaginatedResponse<StudentFeesDetail>>(
|
||||
"/student-fees",
|
||||
params as Record<string, string | number | boolean | undefined>,
|
||||
);
|
||||
},
|
||||
|
||||
async listFeesTerms(params?: PaginationParams): Promise<PaginatedResponse<FeesTerms>> {
|
||||
return api.get<PaginatedResponse<FeesTerms>>("/fees-terms", params as Record<string, string | number | boolean | undefined>);
|
||||
return api.get<PaginatedResponse<FeesTerms>>(
|
||||
"/fees-terms",
|
||||
params as Record<string, string | number | boolean | undefined>,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,15 +16,36 @@ export interface GenerationParams {
|
||||
question_count?: number;
|
||||
}
|
||||
|
||||
export interface PassageGenerationParams {
|
||||
export interface PersonaContext {
|
||||
exam_mode?: "official" | "practice" | string;
|
||||
exam_title?: string;
|
||||
exam_label?: string;
|
||||
structure_name?: string;
|
||||
passage_type?: string;
|
||||
task_type?: string;
|
||||
section_type?: string;
|
||||
part?: string;
|
||||
category?: string;
|
||||
subject_name?: string;
|
||||
subject_id?: number;
|
||||
entity_id?: number;
|
||||
entity_name?: string;
|
||||
rubric_name?: string;
|
||||
rubric_id?: number | string;
|
||||
grading_system?: string;
|
||||
access_type?: string;
|
||||
course_id?: number;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
export interface PassageGenerationParams extends PersonaContext {
|
||||
topic?: string;
|
||||
difficulty?: string;
|
||||
word_count?: number;
|
||||
category?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ExerciseConfig {
|
||||
export interface ExerciseConfig extends PersonaContext {
|
||||
passage_index: number;
|
||||
exercise_types: string[];
|
||||
count_per_type?: number;
|
||||
@@ -106,28 +127,43 @@ export const generationService = {
|
||||
});
|
||||
},
|
||||
|
||||
generateExercises(module: ExamModule, config: ExerciseConfig & { passage_text?: string; type_counts?: Record<string, number>; difficulty?: string }): Promise<{ questions: unknown[] }> {
|
||||
generateExercises(
|
||||
module: ExamModule,
|
||||
config: ExerciseConfig & {
|
||||
passage_text?: string;
|
||||
type_counts?: Record<string, number>;
|
||||
type_instructions?: Record<string, string>;
|
||||
type_difficulties?: Record<string, string>;
|
||||
difficulty?: string;
|
||||
},
|
||||
): Promise<{ questions: unknown[] }> {
|
||||
return api.post(`/exam/${module}/generate`, {
|
||||
...config,
|
||||
generate_exercises: true,
|
||||
});
|
||||
},
|
||||
|
||||
generateWritingInstructions(params: { topic?: string; difficulty?: string; task_type?: string }): Promise<{ instructions: string }> {
|
||||
generateWritingInstructions(
|
||||
params: PersonaContext & { topic?: string; difficulty?: string; task_type?: string; word_limit?: number },
|
||||
): Promise<{ instructions: string }> {
|
||||
return api.post("/exam/writing/generate", {
|
||||
...params,
|
||||
generate_instructions: true,
|
||||
});
|
||||
},
|
||||
|
||||
generateSpeakingScript(params: { topics?: string[]; difficulty?: string; part?: string }): Promise<{ script: string }> {
|
||||
generateSpeakingScript(
|
||||
params: PersonaContext & { topics?: string[]; difficulty?: string; part?: string },
|
||||
): Promise<{ script: string }> {
|
||||
return api.post("/exam/speaking/generate", {
|
||||
...params,
|
||||
generate_script: true,
|
||||
});
|
||||
},
|
||||
|
||||
generateListeningContext(params: { topic?: string; section_type?: string }): Promise<{ context: string }> {
|
||||
generateListeningContext(
|
||||
params: PersonaContext & { topic?: string; section_type?: string; difficulty?: string },
|
||||
): Promise<{ context: string }> {
|
||||
return api.post("/exam/listening/generate", {
|
||||
...params,
|
||||
generate_context: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const gradebookService = {
|
||||
return api.get<PaginatedResponse<Gradebook>>("/gradebooks", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
|
||||
async listGradebookLines(params?: PaginationParams): Promise<PaginatedResponse<GradebookLine>> {
|
||||
async listGradebookLines(params?: PaginationParams & { gradebook_id?: number }): Promise<PaginatedResponse<GradebookLine>> {
|
||||
return api.get<PaginatedResponse<GradebookLine>>("/gradebook-lines", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ import type {
|
||||
LmsTeacherRecord,
|
||||
LmsStudentCreateRequest,
|
||||
LmsTeacherCreateRequest,
|
||||
MyEnrolledCourse,
|
||||
EnrollStudentRequest,
|
||||
BulkEnrollRequest,
|
||||
} from "@/types";
|
||||
|
||||
function mapCourseRaw(r: Record<string, unknown>): Course {
|
||||
@@ -27,6 +30,16 @@ function mapCourseRaw(r: Record<string, unknown>): Course {
|
||||
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] || "") : "",
|
||||
encoach_subject_id: (r.encoach_subject_id as number | null) ?? null,
|
||||
encoach_subject_name: (r.encoach_subject_name as string) || "",
|
||||
topic_ids: (r.topic_ids as number[]) || [],
|
||||
topic_names: (r.topic_names as string[]) || [],
|
||||
learning_objective_ids: (r.learning_objective_ids as number[]) || [],
|
||||
learning_objective_names: (r.learning_objective_names as string[]) || [],
|
||||
tag_ids: (r.tag_ids as number[]) || [],
|
||||
tags: (r.tags as { id: number; name: string; color: string }[]) || [],
|
||||
difficulty_level: (r.difficulty_level as Course["difficulty_level"]) || "",
|
||||
cefr_level: (r.cefr_level as string) || "",
|
||||
instructor_id: 0,
|
||||
instructor_name: "",
|
||||
description: (r.description as string) || "",
|
||||
@@ -37,6 +50,9 @@ function mapCourseRaw(r: Record<string, unknown>): Course {
|
||||
status,
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
chapter_count: Number(r.chapter_count ?? 0),
|
||||
resource_count: Number(r.resource_count ?? 0),
|
||||
objective_count: Number(r.objective_count ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,8 +70,8 @@ function mapBatchRaw(r: Record<string, unknown>): Batch {
|
||||
course_name: String(r.course_name || ""),
|
||||
teacher_id: 0,
|
||||
teacher_name: "",
|
||||
student_ids: [],
|
||||
student_count: 0,
|
||||
student_ids: ((r.students as { id: number }[]) ?? []).map((s) => s.id),
|
||||
student_count: Number(r.student_count ?? 0),
|
||||
start_date: String(r.start_date || ""),
|
||||
end_date: String(r.end_date || ""),
|
||||
schedule: "",
|
||||
@@ -101,6 +117,18 @@ export function lmsTeacherToUser(t: LmsTeacherRecord): User {
|
||||
}
|
||||
|
||||
export const lmsService = {
|
||||
async getMyEnrolledCourses(): Promise<{ items: MyEnrolledCourse[]; total: number }> {
|
||||
return api.get("/student/my-courses");
|
||||
},
|
||||
|
||||
async enrollStudent(studentId: number, data: EnrollStudentRequest): Promise<{ success: boolean; enrolled_course_ids: number[] }> {
|
||||
return api.post(`/students/${studentId}/enroll`, data);
|
||||
},
|
||||
|
||||
async bulkEnrollInCourse(courseId: number, data: BulkEnrollRequest): Promise<{ success: boolean; enrolled_student_ids: number[]; total_enrolled: number }> {
|
||||
return api.post(`/courses/${courseId}/enroll`, data);
|
||||
},
|
||||
|
||||
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);
|
||||
@@ -113,13 +141,20 @@ export const lmsService = {
|
||||
},
|
||||
|
||||
async createCourse(data: CourseCreateRequest): Promise<Course> {
|
||||
const raw = await api.post<unknown>("/courses", {
|
||||
const body: Record<string, unknown> = {
|
||||
name: data.title,
|
||||
code: data.code,
|
||||
description: data.description,
|
||||
max_capacity: data.max_capacity,
|
||||
status: "draft",
|
||||
});
|
||||
};
|
||||
if (data.encoach_subject_id) body.encoach_subject_id = data.encoach_subject_id;
|
||||
if (data.topic_ids?.length) body.topic_ids = data.topic_ids;
|
||||
if (data.learning_objective_ids?.length) body.learning_objective_ids = data.learning_objective_ids;
|
||||
if (data.tag_ids?.length) body.tag_ids = data.tag_ids;
|
||||
if (data.difficulty_level) body.difficulty_level = data.difficulty_level;
|
||||
if (data.cefr_level) body.cefr_level = data.cefr_level;
|
||||
const raw = await api.post<unknown>("/courses", body);
|
||||
return mapCourseRaw(asRecordData<Record<string, unknown>>(raw));
|
||||
},
|
||||
|
||||
@@ -129,6 +164,12 @@ export const lmsService = {
|
||||
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;
|
||||
if (data.encoach_subject_id !== undefined) body.encoach_subject_id = data.encoach_subject_id || null;
|
||||
if (data.topic_ids !== undefined) body.topic_ids = data.topic_ids;
|
||||
if (data.learning_objective_ids !== undefined) body.learning_objective_ids = data.learning_objective_ids;
|
||||
if (data.tag_ids !== undefined) body.tag_ids = data.tag_ids;
|
||||
if (data.difficulty_level !== undefined) body.difficulty_level = data.difficulty_level;
|
||||
if (data.cefr_level !== undefined) body.cefr_level = data.cefr_level;
|
||||
const raw = await api.patch<unknown>(`/courses/${id}`, body);
|
||||
return mapCourseRaw(asRecordData<Record<string, unknown>>(raw));
|
||||
},
|
||||
@@ -196,6 +237,26 @@ export const lmsService = {
|
||||
return asRecordData<LmsTeacherRecord>(raw);
|
||||
},
|
||||
|
||||
async deleteTeacher(id: number): Promise<ApiSuccessResponse> {
|
||||
return api.delete<ApiSuccessResponse>(`/teachers/${id}`);
|
||||
},
|
||||
|
||||
async getBatchStudents(batchId: number): Promise<{ data: { id: number; name: string; email: string; course_id: number; course_name: string }[]; total: number }> {
|
||||
return api.get(`/batches/${batchId}/students`);
|
||||
},
|
||||
|
||||
async addStudentsToBatch(batchId: number, studentIds: number[]): Promise<{ success: boolean; added_ids: number[]; total: number }> {
|
||||
return api.post(`/batches/${batchId}/students`, { student_ids: studentIds });
|
||||
},
|
||||
|
||||
async removeStudentsFromBatch(batchId: number, studentIds: number[]): Promise<{ success: boolean; removed_ids: number[]; total: number }> {
|
||||
return api.post(`/batches/${batchId}/students/remove`, { student_ids: studentIds });
|
||||
},
|
||||
|
||||
async deleteStudent(id: number): Promise<ApiSuccessResponse> {
|
||||
return api.delete<ApiSuccessResponse>(`/students/${id}`);
|
||||
},
|
||||
|
||||
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 ?? [];
|
||||
@@ -222,4 +283,34 @@ export const lmsService = {
|
||||
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 ?? [];
|
||||
},
|
||||
|
||||
async getCourseTaxonomy(courseId: number): Promise<{
|
||||
subject: { id: number; name: string; code: string } | null;
|
||||
domains: { id: number; name: string }[];
|
||||
topics: { id: number; name: string; domain_id: number; domain_name: string }[];
|
||||
objectives: { id: number; name: string; topic_id: number; topic_name: string; bloom_level: string }[];
|
||||
tags: { id: number; name: string; color: string }[];
|
||||
}> {
|
||||
return api.get(`/courses/${courseId}/taxonomy`);
|
||||
},
|
||||
|
||||
async getSubjectCourses(subjectId: number): Promise<{ items: Course[]; total: number }> {
|
||||
const raw = await api.get<unknown>(`/subjects/${subjectId}/courses`);
|
||||
const p = asPaginated<Record<string, unknown>>(raw);
|
||||
return { ...p, items: p.items.map(mapCourseRaw) };
|
||||
},
|
||||
|
||||
async listLearningObjectives(params?: { topic_id?: number; topic_ids?: string; subject_id?: number }): Promise<{
|
||||
items: { id: number; name: string; description: string; topic_id: number; topic_name: string; bloom_level: string; cefr_level: string }[];
|
||||
total: number;
|
||||
}> {
|
||||
return api.get("/learning-objectives", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
|
||||
async listDomains(params?: { subject_id?: number }): Promise<{
|
||||
items: { id: number; name: string; subject_id: number; subject_name: string; description: string }[];
|
||||
total: number;
|
||||
}> {
|
||||
return api.get("/domains", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
};
|
||||
|
||||
63
frontend/src/services/payments.service.ts
Normal file
63
frontend/src/services/payments.service.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
export interface PaymentRecord {
|
||||
id: number;
|
||||
ref: string;
|
||||
student_id: number | null;
|
||||
student_name: string;
|
||||
course_id: number | null;
|
||||
course_name: string;
|
||||
product_name: string;
|
||||
amount: number;
|
||||
after_discount_amount: number;
|
||||
currency: string;
|
||||
state: string;
|
||||
paid: boolean;
|
||||
date: string;
|
||||
type: string;
|
||||
invoice_id: number | null;
|
||||
invoice_number: string;
|
||||
payment_state: string;
|
||||
}
|
||||
|
||||
export interface PaymentTotals {
|
||||
count: number;
|
||||
paid: number;
|
||||
unpaid: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface PaymentListResponse {
|
||||
data: PaymentRecord[];
|
||||
items: PaymentRecord[];
|
||||
total: number;
|
||||
totals: PaymentTotals;
|
||||
}
|
||||
|
||||
export interface PaymobOrder {
|
||||
id: string | number;
|
||||
status: string;
|
||||
user: string;
|
||||
email: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface PaymobListResponse {
|
||||
data: PaymobOrder[];
|
||||
items: PaymobOrder[];
|
||||
total: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const paymentsService = {
|
||||
async list(params?: { status?: string; paid?: string; student_id?: number }) {
|
||||
return api.get<PaymentListResponse>(
|
||||
"/payment-records",
|
||||
params as Record<string, string | number | boolean | undefined>,
|
||||
);
|
||||
},
|
||||
|
||||
async paymobOrders() {
|
||||
return api.get<PaymobListResponse>("/paymob-orders");
|
||||
},
|
||||
};
|
||||
59
frontend/src/services/platformSettings.service.ts
Normal file
59
frontend/src/services/platformSettings.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { api } from "@/lib/api-client";
|
||||
|
||||
export interface RegistrationCode {
|
||||
id: number;
|
||||
code: string;
|
||||
code_type: "individual" | "corporate";
|
||||
user_type: "student" | "teacher" | "corporate";
|
||||
max_uses: number;
|
||||
uses: number;
|
||||
used: boolean;
|
||||
expiry_date: string;
|
||||
created: string;
|
||||
entity_id: number | null;
|
||||
}
|
||||
|
||||
export interface GeneratePayload {
|
||||
count?: number;
|
||||
code_type?: "individual" | "corporate";
|
||||
user_type?: "student" | "teacher" | "corporate";
|
||||
max_uses?: number;
|
||||
expiry_date?: string;
|
||||
}
|
||||
|
||||
export interface PlatformPackage {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
duration: string;
|
||||
discount: number;
|
||||
}
|
||||
|
||||
export interface GradingConfig {
|
||||
min_score: number;
|
||||
max_score: number;
|
||||
increment: number;
|
||||
}
|
||||
|
||||
export const platformSettingsService = {
|
||||
listCodes: () =>
|
||||
api.get<{ items: RegistrationCode[]; total: number }>("/codes"),
|
||||
generateCodes: (payload: GeneratePayload) =>
|
||||
api.post<{ data: RegistrationCode[]; count: number }>(
|
||||
"/codes/generate",
|
||||
payload,
|
||||
),
|
||||
deleteCode: (id: number) => api.delete<{ success: boolean }>(`/codes/${id}`),
|
||||
|
||||
listPackages: () =>
|
||||
api.get<{ items: PlatformPackage[]; total: number }>("/packages"),
|
||||
createPackage: (p: Omit<PlatformPackage, "id">) =>
|
||||
api.post<PlatformPackage>("/packages", p),
|
||||
updatePackage: (id: number, p: Partial<PlatformPackage>) =>
|
||||
api.patch<PlatformPackage>(`/packages/${id}`, p),
|
||||
deletePackage: (id: number) =>
|
||||
api.delete<{ success: boolean }>(`/packages/${id}`),
|
||||
|
||||
getGrading: () => api.get<GradingConfig>("/grading-config"),
|
||||
setGrading: (cfg: GradingConfig) => api.patch<GradingConfig>("/grading-config", cfg),
|
||||
};
|
||||
@@ -1,11 +1,15 @@
|
||||
import { api, API_BASE_URL } from "@/lib/api-client";
|
||||
import { asPaginated, asRecordData } from "@/lib/odoo-api";
|
||||
import type { Resource, ResourceCompletion, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
|
||||
import type { Resource, ResourceTag, ResourceCompletion, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
|
||||
|
||||
export interface ResourceListParams extends PaginationParams {
|
||||
subject_id?: number;
|
||||
topic_id?: number;
|
||||
tag_id?: number;
|
||||
resource_type?: string;
|
||||
review_status?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
@@ -45,4 +49,24 @@ export const resourcesService = {
|
||||
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
||||
return res.blob();
|
||||
},
|
||||
|
||||
// Tag management
|
||||
async listTags(): Promise<ResourceTag[]> {
|
||||
const res = await api.get<{ data?: ResourceTag[]; items?: ResourceTag[] }>("/resource-tags");
|
||||
return res.data ?? res.items ?? [];
|
||||
},
|
||||
|
||||
async createTag(data: { name: string; color?: string; description?: string }): Promise<ResourceTag> {
|
||||
const res = await api.post<{ data: ResourceTag }>("/resource-tags", data);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async updateTag(id: number, data: Partial<ResourceTag>): Promise<ResourceTag> {
|
||||
const res = await api.patch<{ data: ResourceTag }>(`/resource-tags/${id}`, data);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async deleteTag(id: number): Promise<ApiSuccessResponse> {
|
||||
return api.delete<ApiSuccessResponse>(`/resource-tags/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { api } from "@/lib/api-client";
|
||||
import type { StudentProgression } from "@/types/student-progress";
|
||||
import type { StudentProgression, StudentProgressDetail } from "@/types/student-progress";
|
||||
import type { PaginatedResponse, PaginationParams } from "@/types";
|
||||
|
||||
export interface StudentProgressFilters extends PaginationParams {
|
||||
q?: string;
|
||||
batch_id?: number;
|
||||
course_id?: number;
|
||||
}
|
||||
|
||||
export const studentProgressService = {
|
||||
async listProgress(params?: PaginationParams): Promise<PaginatedResponse<StudentProgression>> {
|
||||
async listProgress(params?: StudentProgressFilters): Promise<PaginatedResponse<StudentProgression>> {
|
||||
return api.get<PaginatedResponse<StudentProgression>>("/student-progress", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
async getProgress(id: number): Promise<StudentProgressDetail> {
|
||||
return api.get<StudentProgressDetail>(`/student-progress/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { Subject, Domain, Topic, LearningObjective, TaxonomyTree, Paginated
|
||||
|
||||
export const taxonomyService = {
|
||||
async listSubjects(): Promise<Subject[]> {
|
||||
const res = await api.get<{ data: Subject[] } | Subject[]>("/subjects");
|
||||
return Array.isArray(res) ? res : (res as { data: Subject[] }).data ?? [];
|
||||
const res = await api.get<Record<string, unknown>>("/subjects");
|
||||
if (Array.isArray(res)) return res;
|
||||
const r = res as { data?: Subject[]; items?: Subject[]; subjects?: Subject[] };
|
||||
return r.data ?? r.items ?? r.subjects ?? [];
|
||||
},
|
||||
|
||||
async getSubject(id: number): Promise<Subject> {
|
||||
@@ -34,8 +36,10 @@ export const taxonomyService = {
|
||||
},
|
||||
|
||||
async listDomains(params?: { subject_id?: number }): Promise<Domain[]> {
|
||||
const res = await api.get<{ data: Domain[] } | Domain[]>("/domains", params as Record<string, string | number | boolean | undefined>);
|
||||
return Array.isArray(res) ? res : (res as { data: Domain[] }).data ?? [];
|
||||
const res = await api.get<Record<string, unknown>>("/domains", params as Record<string, string | number | boolean | undefined>);
|
||||
if (Array.isArray(res)) return res;
|
||||
const r = res as { data?: Domain[]; items?: Domain[] };
|
||||
return r.data ?? r.items ?? [];
|
||||
},
|
||||
|
||||
async createDomain(data: Partial<Domain>): Promise<Domain> {
|
||||
@@ -55,8 +59,10 @@ export const taxonomyService = {
|
||||
},
|
||||
|
||||
async listTopics(params?: { domain_id?: number; subject_id?: number }): Promise<Topic[]> {
|
||||
const res = await api.get<{ data: Topic[] } | Topic[]>("/topics", params as Record<string, string | number | boolean | undefined>);
|
||||
return Array.isArray(res) ? res : (res as { data: Topic[] }).data ?? [];
|
||||
const res = await api.get<Record<string, unknown>>("/topics", params as Record<string, string | number | boolean | undefined>);
|
||||
if (Array.isArray(res)) return res;
|
||||
const r = res as { data?: Topic[]; items?: Topic[] };
|
||||
return r.data ?? r.items ?? [];
|
||||
},
|
||||
|
||||
async createTopic(data: Partial<Topic>): Promise<Topic> {
|
||||
|
||||
@@ -1,16 +1,110 @@
|
||||
import { api } from "@/lib/api-client";
|
||||
import type { Training, TrainingTip, Walkthrough } from "@/types";
|
||||
|
||||
export type CefrLevel = "A1" | "A2" | "B1" | "B2" | "C1" | "C2";
|
||||
|
||||
export interface VocabItem {
|
||||
id: number;
|
||||
word: string;
|
||||
meaning: string;
|
||||
example_sentence: string;
|
||||
level: CefrLevel;
|
||||
part_of_speech: "noun" | "verb" | "adjective" | "adverb" | "phrase" | "other";
|
||||
category: string;
|
||||
active: boolean;
|
||||
learners_count: number;
|
||||
completion_count: number;
|
||||
completed: boolean;
|
||||
mastery: "learning" | "familiar" | "mastered";
|
||||
last_reviewed: string;
|
||||
}
|
||||
|
||||
export interface GrammarRule {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
example: string;
|
||||
level: CefrLevel;
|
||||
category: string;
|
||||
active: boolean;
|
||||
learners_count: number;
|
||||
completion_count: number;
|
||||
completed: boolean;
|
||||
last_reviewed: string;
|
||||
}
|
||||
|
||||
export interface TrainingSummary {
|
||||
total: number;
|
||||
completed: number;
|
||||
remaining: number;
|
||||
completion_rate: number;
|
||||
}
|
||||
|
||||
export interface VocabListResponse {
|
||||
items: VocabItem[];
|
||||
total: number;
|
||||
summary: TrainingSummary;
|
||||
}
|
||||
|
||||
export interface GrammarListResponse {
|
||||
items: GrammarRule[];
|
||||
total: number;
|
||||
summary: TrainingSummary;
|
||||
}
|
||||
|
||||
export interface VocabFilters {
|
||||
level?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
active?: string;
|
||||
}
|
||||
|
||||
export interface GrammarFilters {
|
||||
level?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
active?: string;
|
||||
}
|
||||
|
||||
export type VocabCreatePayload = Omit<
|
||||
VocabItem,
|
||||
"id" | "completed" | "mastery" | "last_reviewed" | "learners_count" | "completion_count"
|
||||
>;
|
||||
|
||||
export type GrammarCreatePayload = Omit<
|
||||
GrammarRule,
|
||||
"id" | "completed" | "last_reviewed" | "learners_count" | "completion_count"
|
||||
>;
|
||||
|
||||
export const trainingService = {
|
||||
async getTraining(params?: { entity_id?: number; user_id?: number; module?: string }): Promise<Training[]> {
|
||||
return api.get<Training[]>("/training", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
// Vocabulary
|
||||
listVocab: (filters?: VocabFilters) =>
|
||||
api.get<VocabListResponse>(
|
||||
"/training/vocabulary",
|
||||
filters as Record<string, string | number | boolean | undefined>,
|
||||
),
|
||||
createVocab: (payload: Partial<VocabCreatePayload>) =>
|
||||
api.post<VocabItem>("/training/vocabulary", payload),
|
||||
updateVocab: (id: number, payload: Partial<VocabItem>) =>
|
||||
api.patch<VocabItem>(`/training/vocabulary/${id}`, payload),
|
||||
deleteVocab: (id: number) =>
|
||||
api.delete<{ success: boolean }>(`/training/vocabulary/${id}`),
|
||||
setVocabProgress: (
|
||||
id: number,
|
||||
payload: { completed?: boolean; mastery?: VocabItem["mastery"] },
|
||||
) => api.post<VocabItem>(`/training/vocabulary/${id}/progress`, payload),
|
||||
|
||||
async getTips(params?: { subject_id?: number; category?: string }): Promise<TrainingTip[]> {
|
||||
return api.get<TrainingTip[]>("/training/tips", params as Record<string, string | number | boolean | undefined>);
|
||||
},
|
||||
|
||||
async getWalkthroughs(module?: string): Promise<Walkthrough[]> {
|
||||
return api.get<Walkthrough[]>("/training/walkthrough", module ? { module } : undefined);
|
||||
},
|
||||
// Grammar
|
||||
listGrammar: (filters?: GrammarFilters) =>
|
||||
api.get<GrammarListResponse>(
|
||||
"/training/grammar",
|
||||
filters as Record<string, string | number | boolean | undefined>,
|
||||
),
|
||||
createGrammar: (payload: Partial<GrammarCreatePayload>) =>
|
||||
api.post<GrammarRule>("/training/grammar", payload),
|
||||
updateGrammar: (id: number, payload: Partial<GrammarRule>) =>
|
||||
api.patch<GrammarRule>(`/training/grammar/${id}`, payload),
|
||||
deleteGrammar: (id: number) =>
|
||||
api.delete<{ success: boolean }>(`/training/grammar/${id}`),
|
||||
setGrammarProgress: (id: number, payload: { completed?: boolean }) =>
|
||||
api.post<GrammarRule>(`/training/grammar/${id}/progress`, payload),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user