feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
parent a3e12f62fa
commit f1c4953a63
731 changed files with 67205 additions and 139 deletions

View File

@@ -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<PaginatedResponse<AcademicYear>> {
return api.get<PaginatedResponse<AcademicYear>>("/academic-years", params as Record<string, string | number | boolean | undefined>);
},
async getAcademicYear(id: number): Promise<AcademicYear> {
return api.get<AcademicYear>(`/academic-years/${id}`);
},
async createAcademicYear(data: AcademicYearCreateRequest): Promise<AcademicYear> {
return api.post<AcademicYear>("/academic-years", data);
},
async updateAcademicYear(id: number, data: Partial<AcademicYearCreateRequest>): Promise<AcademicYear> {
return api.patch<AcademicYear>(`/academic-years/${id}`, data);
},
async deleteAcademicYear(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/academic-years/${id}`);
},
async generateTerms(yearId: number): Promise<AcademicTerm[]> {
return api.post<AcademicTerm[]>(`/academic-years/${yearId}/generate-terms`);
},
async listTerms(params?: PaginationParams & { year_id?: number }): Promise<PaginatedResponse<AcademicTerm>> {
return api.get<PaginatedResponse<AcademicTerm>>("/academic-terms", params as Record<string, string | number | boolean | undefined>);
},
async createTerm(data: AcademicTermCreateRequest): Promise<AcademicTerm> {
return api.post<AcademicTerm>("/academic-terms", data);
},
async updateTerm(id: number, data: Partial<AcademicTermCreateRequest>): Promise<AcademicTerm> {
return api.patch<AcademicTerm>(`/academic-terms/${id}`, data);
},
async deleteTerm(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/academic-terms/${id}`);
},
async listDepartments(params?: PaginationParams): Promise<PaginatedResponse<Department>> {
return api.get<PaginatedResponse<Department>>("/departments", params as Record<string, string | number | boolean | undefined>);
},
async createDepartment(data: DepartmentCreateRequest): Promise<Department> {
return api.post<Department>("/departments", data);
},
async updateDepartment(id: number, data: Partial<DepartmentCreateRequest>): Promise<Department> {
return api.patch<Department>(`/departments/${id}`, data);
},
async deleteDepartment(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/departments/${id}`);
},
};

View File

@@ -0,0 +1,33 @@
import { api } from "@/lib/api-client";
import type { Activity, ActivityType, ActivityCreateRequest } from "@/types/activity";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const activityService = {
async listActivities(params?: PaginationParams): Promise<PaginatedResponse<Activity>> {
return api.get<PaginatedResponse<Activity>>("/activities", params as Record<string, string | number | boolean | undefined>);
},
async createActivity(data: ActivityCreateRequest): Promise<Activity> {
return api.post<Activity>("/activities", data);
},
async updateActivity(id: number, data: Partial<ActivityCreateRequest>): Promise<Activity> {
return api.patch<Activity>(`/activities/${id}`, data);
},
async deleteActivity(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/activities/${id}`);
},
async listActivityTypes(params?: PaginationParams): Promise<PaginatedResponse<ActivityType>> {
return api.get<PaginatedResponse<ActivityType>>("/activity-types", params as Record<string, string | number | boolean | undefined>);
},
async createActivityType(data: Pick<ActivityType, "name">): Promise<ActivityType> {
return api.post<ActivityType>("/activity-types", data);
},
async deleteActivityType(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/activity-types/${id}`);
},
};

View File

@@ -0,0 +1,30 @@
import { api } from "@/lib/api-client";
import type {
AdaptiveDashboardMetrics,
AdaptiveEngineStudentRow,
AdaptiveThresholdSettings,
StudentAbilityModel,
StudentAdaptiveSignal,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const adaptiveEngineService = {
getDashboard: () => api.get<AdaptiveDashboardMetrics>("/adaptive-engine/dashboard"),
getStudents: (params?: { page?: number; limit?: number }) =>
api.get<{ data: AdaptiveEngineStudentRow[]; pagination?: { total: number; page: number } }>(
"/adaptive-engine/students",
params as Record<string, string | number | boolean | undefined>,
),
getStudentSignals: (studentId: number) =>
api.get<StudentAdaptiveSignal[]>(`/adaptive-engine/students/${studentId}/signals`),
getStudentAbility: (studentId: number) =>
api.get<StudentAbilityModel>(`/adaptive-engine/students/${studentId}/ability`),
getSettings: () => api.get<AdaptiveThresholdSettings>("/adaptive-engine/settings"),
updateSettings: (data: AdaptiveThresholdSettings) =>
api.put<ApiSuccessResponse>("/adaptive-engine/settings", data),
};

View File

@@ -0,0 +1,78 @@
import { api } from "@/lib/api-client";
import type {
Proficiency, ProficiencySummary, LearningPlan,
DiagnosticSession, DiagnosticQuestion, DiagnosticAnswer, DiagnosticResult,
TopicContent, ApiSuccessResponse,
} from "@/types";
export const adaptiveService = {
async startDiagnostic(subjectId: number): Promise<{ session: DiagnosticSession; first_question: DiagnosticQuestion }> {
return api.post("/diagnostic/start", { subject_id: subjectId });
},
async answerDiagnostic(data: DiagnosticAnswer): Promise<{ next_question?: DiagnosticQuestion; completed: boolean }> {
return api.post("/diagnostic/answer", data);
},
async getDiagnosticResult(sessionId: number): Promise<DiagnosticResult> {
return api.get(`/diagnostic/${sessionId}/result`);
},
async getProficiency(params: { subject_id?: number }): Promise<Proficiency[]> {
const raw = await api.get<{ data: Proficiency[] } | Proficiency[]>("/proficiency", params as Record<string, string | number | boolean | undefined>);
return Array.isArray(raw) ? raw : (raw as { data: Proficiency[] }).data ?? [];
},
async getProficiencySummary(): Promise<ProficiencySummary[]> {
const raw = await api.get<{ data: ProficiencySummary[] } | ProficiencySummary[]>("/proficiency/summary");
return Array.isArray(raw) ? raw : (raw as { data: ProficiencySummary[] }).data ?? [];
},
async getClassProficiency(params?: { subject_id?: number }): Promise<unknown> {
return api.get("/proficiency/class", params as Record<string, string | number | boolean | undefined>);
},
async getLearningPlan(params: { subject_id?: number }): Promise<LearningPlan> {
return api.get<LearningPlan>("/learning-plan", params as Record<string, string | number | boolean | undefined>);
},
async generateLearningPlan(data: { subject_id: number; target_completion?: string }): Promise<LearningPlan> {
return api.post<LearningPlan>("/learning-plan/generate", data);
},
async updateLearningPlan(id: number, data: Partial<LearningPlan>): Promise<LearningPlan> {
return api.patch<LearningPlan>(`/learning-plan/${id}`, data);
},
async pauseLearningPlan(id: number): Promise<LearningPlan> {
return api.post<LearningPlan>(`/learning-plan/${id}/pause`);
},
async resumeLearningPlan(id: number): Promise<LearningPlan> {
return api.post<LearningPlan>(`/learning-plan/${id}/resume`);
},
async getTopicContent(topicId: number): Promise<TopicContent> {
return api.get<TopicContent>(`/topics/${topicId}/content`);
},
async generateTopicContent(topicId: number): Promise<TopicContent> {
return api.post<TopicContent>(`/topics/${topicId}/generate-content`);
},
async getPracticeQuestions(topicId: number): Promise<{ questions: unknown[] }> {
return api.post(`/topics/${topicId}/practice`);
},
async gradePractice(topicId: number, answers: unknown[]): Promise<{ results: unknown[]; mastery_update: number }> {
return api.post(`/topics/${topicId}/practice/grade`, { answers });
},
async startMasteryQuiz(topicId: number): Promise<{ questions: unknown[] }> {
return api.post(`/topics/${topicId}/mastery-quiz`);
},
async submitMasteryQuiz(topicId: number, answers: unknown[]): Promise<{ passed: boolean; score: number; mastery_update: number }> {
return api.post(`/topics/${topicId}/mastery-quiz/submit`, { answers });
},
};

View File

@@ -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<PaginatedResponse<AdmissionRegister>> {
return api.get<PaginatedResponse<AdmissionRegister>>("/admission-registers", params as Record<string, string | number | boolean | undefined>);
},
async createRegister(data: AdmissionRegisterCreateRequest): Promise<AdmissionRegister> {
return api.post<AdmissionRegister>("/admission-registers", data);
},
async updateRegister(id: number, data: Partial<AdmissionRegisterCreateRequest>): Promise<AdmissionRegister> {
return api.patch<AdmissionRegister>(`/admission-registers/${id}`, data);
},
async confirmRegister(id: number): Promise<AdmissionRegister> {
return api.post<AdmissionRegister>(`/admission-registers/${id}/confirm`);
},
async closeRegister(id: number): Promise<AdmissionRegister> {
return api.post<AdmissionRegister>(`/admission-registers/${id}/close`);
},
async listAdmissions(params?: PaginationParams & { state?: string; register_id?: number; course_id?: number }): Promise<PaginatedResponse<Admission>> {
return api.get<PaginatedResponse<Admission>>("/admissions", params as Record<string, string | number | boolean | undefined>);
},
async getAdmission(id: number): Promise<Admission> {
return api.get<Admission>(`/admissions/${id}`);
},
async createAdmission(data: AdmissionCreateRequest): Promise<Admission> {
return api.post<Admission>("/admissions", data);
},
async submitAdmission(id: number): Promise<Admission> {
return api.post<Admission>(`/admissions/${id}/submit`);
},
async confirmAdmission(id: number): Promise<Admission> {
return api.post<Admission>(`/admissions/${id}/confirm`);
},
async admitAdmission(id: number): Promise<Admission> {
return api.post<Admission>(`/admissions/${id}/admit`);
},
async rejectAdmission(id: number): Promise<Admission> {
return api.post<Admission>(`/admissions/${id}/reject`);
},
};

View File

@@ -0,0 +1,41 @@
import { api } from "@/lib/api-client";
import type {
AICourseConfig,
QualityGateResult,
IELTSValidationResult,
ExaminerReview,
AICourseTrack,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const aiCourseService = {
createEnglish: (data: { current_level: string; target_level: string; learning_style: string[] }) =>
api.post<{ course_id: number }>("/ai-course/english/create", data),
createIelts: (data: { exam_type: string; target_band: number; skills: string[] }) =>
api.post<{ course_id: number }>("/ai-course/ielts/create", data),
getCourse: (courseId: number) =>
api.get<AICourseConfig>(`/ai-course/${courseId}`),
getTracks: (courseId: number) =>
api.get<AICourseTrack[]>(`/ai-course/${courseId}/tracks`),
getQualityGate: (courseId: number) =>
api.get<QualityGateResult>(`/ai-course/${courseId}/quality`),
approveQuality: (courseId: number) =>
api.post<ApiSuccessResponse>(`/ai-course/${courseId}/quality/approve`),
rejectQuality: (courseId: number, notes: string) =>
api.post<ApiSuccessResponse>(`/ai-course/${courseId}/quality/reject`, { notes }),
getIeltsValidation: (courseId: number) =>
api.get<IELTSValidationResult>(`/ai-course/${courseId}/validation`),
submitExaminerReview: (data: ExaminerReview) =>
api.post<ApiSuccessResponse>(`/ai-course/examiner-review`, data),
getEnglishTaxonomy: () =>
api.get<Record<string, unknown>>("/ai-course/english/taxonomy"),
};

View File

@@ -0,0 +1,44 @@
import { api } from "@/lib/api-client";
import type { AiInsight, AiAlert, AiSearchResult, AiBatchOptimization, AiGradingResult } from "@/types";
export const analyticsService = {
async getStudentAnalytics(params?: Record<string, string | number | boolean | undefined>): Promise<unknown> {
return api.get("/analytics/student", params);
},
async getClassAnalytics(params?: Record<string, string | number | boolean | undefined>): Promise<unknown> {
return api.get("/analytics/class", params);
},
async getSubjectAnalytics(params?: Record<string, string | number | boolean | undefined>): Promise<unknown> {
return api.get("/analytics/subject", params);
},
async getContentGaps(params?: { subject_id?: number }): Promise<{ gaps: { topic_id: number; topic_name: string; resource_count: number }[] }> {
return api.get("/analytics/content-gaps", params as Record<string, string | number | boolean | undefined>);
},
async search(query: string): Promise<AiSearchResult[]> {
return api.post<AiSearchResult[]>("/ai/search", { query });
},
async getInsights(data: Record<string, unknown>): Promise<AiInsight[]> {
return api.post<AiInsight[]>("/ai/insights", data);
},
async getAlerts(): Promise<AiAlert[]> {
return api.get<AiAlert[]>("/ai/alerts");
},
async getReportNarrative(data: { report_type: string; data: Record<string, unknown> }): Promise<{ narrative: string }> {
return api.post("/ai/report-narrative", data);
},
async getBatchOptimization(batchId: number): Promise<AiBatchOptimization[]> {
return api.post<AiBatchOptimization[]>("/ai/batch-optimize", { batch_id: batchId });
},
async getGradingSuggestion(data: { submission_id: number; text: string; rubric_id?: number }): Promise<AiGradingResult> {
return api.post<AiGradingResult>("/ai/grade-suggest", data);
},
};

View File

@@ -0,0 +1,30 @@
import { api } from "@/lib/api-client";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export interface ApprovalWorkflow {
id: number;
name: string;
entity_id: number;
entity_name: string;
steps: { order: number; approver_id: number; approver_name: string }[];
status: "active" | "draft";
created_at: string;
}
export const approvalsService = {
async list(params?: PaginationParams): Promise<PaginatedResponse<ApprovalWorkflow>> {
return api.get<PaginatedResponse<ApprovalWorkflow>>("/approval-workflows", params as Record<string, string | number | boolean | undefined>);
},
async create(data: Partial<ApprovalWorkflow>): Promise<ApprovalWorkflow> {
return api.post<ApprovalWorkflow>("/approval-workflows", data);
},
async update(id: number, data: Partial<ApprovalWorkflow>): Promise<ApprovalWorkflow> {
return api.patch<ApprovalWorkflow>(`/approval-workflows/${id}`, data);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/approval-workflows/${id}`);
},
};

View File

@@ -0,0 +1,37 @@
import { api } from "@/lib/api-client";
import type { Assignment, AssignmentState, AssignmentCreateRequest, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export interface AssignmentListParams extends PaginationParams {
state?: AssignmentState;
entity_id?: number;
}
export const assignmentsService = {
async list(params?: AssignmentListParams): Promise<PaginatedResponse<Assignment>> {
return api.get<PaginatedResponse<Assignment>>("/assignments", params as Record<string, string | number | boolean | undefined>);
},
async getById(id: number): Promise<Assignment> {
return api.get<Assignment>(`/assignments/${id}`);
},
async create(data: AssignmentCreateRequest): Promise<Assignment> {
return api.post<Assignment>("/assignments", data);
},
async update(id: number, data: Partial<AssignmentCreateRequest>): Promise<Assignment> {
return api.patch<Assignment>(`/assignments/${id}`, data);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/assignments/${id}`);
},
async archive(id: number): Promise<Assignment> {
return api.post<Assignment>(`/assignments/${id}/archive`);
},
async start(id: number): Promise<Assignment> {
return api.post<Assignment>(`/assignments/${id}/start`);
},
};

View File

@@ -0,0 +1,30 @@
import { api, setToken, clearToken } from "@/lib/api-client";
import type { LoginRequest, LoginResponse, User, ResetPasswordRequest, CurrentUserResponse } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const authService = {
async login(data: LoginRequest): Promise<LoginResponse> {
// Odoo accepts `login` or `email` in JSON body
const res = await api.post<LoginResponse>("/login", { ...data, email: data.login });
setToken(res.token);
return res;
},
async logout(): Promise<void> {
await api.post<void>("/logout").catch(() => {});
clearToken();
},
async getUser(): Promise<User> {
const res = await api.get<CurrentUserResponse>("/user");
return res.user;
},
async resetPassword(data: ResetPasswordRequest): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>("/reset/sendVerification", data);
},
async setInvitePassword(data: { token: string; password: string }): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>("/auth/invite/set-password", data);
},
};

View File

@@ -0,0 +1,29 @@
import { api } from "@/lib/api-client";
import type { BrandingConfig, EntityBranding } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const brandingService = {
get: (entityId: number) =>
api.get<EntityBranding>(`/entity/${entityId}/branding`),
update: (entityId: number, data: BrandingConfig) =>
api.put<ApiSuccessResponse>(`/entity/${entityId}/branding`, data),
resetToDefault: (entityId: number) =>
api.post<ApiSuccessResponse>(`/entity/${entityId}/branding/reset`),
getCurrentBranding: () =>
api.get<EntityBranding>("/entity/branding"),
uploadLogo: (entityId: number, file: File) => {
const formData = new FormData();
formData.append("logo", file);
return api.upload<{ url: string }>(`/entity/${entityId}/branding/logo`, formData);
},
uploadFavicon: (entityId: number, file: File) => {
const formData = new FormData();
formData.append("favicon", file);
return api.upload<{ url: string }>(`/entity/${entityId}/branding/favicon`, formData);
},
};

View File

@@ -0,0 +1,36 @@
import { api } from "@/lib/api-client";
import type { Classroom, ClassroomCreateRequest, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export interface ClassroomListParams extends PaginationParams {
entity_id?: number;
}
export const classroomsService = {
async list(params?: ClassroomListParams): Promise<PaginatedResponse<Classroom>> {
return api.get<PaginatedResponse<Classroom>>("/groups", params as Record<string, string | number | boolean | undefined>);
},
async getById(id: number): Promise<Classroom> {
return api.get<Classroom>(`/groups/${id}`);
},
async create(data: ClassroomCreateRequest): Promise<Classroom> {
return api.post<Classroom>("/groups", data);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/groups/${id}`);
},
async transfer(data: { student_ids: number[]; from_id: number; to_id: number }): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>("/groups/transfer", data);
},
async addMembers(id: number, userIds: number[]): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/groups/${id}/members`, { user_ids: userIds });
},
async removeMembers(id: number, userIds: number[]): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/groups/${id}/members/remove`, { user_ids: userIds });
},
};

View File

@@ -0,0 +1,28 @@
import { api } from "@/lib/api-client";
import type { AiChatRequest, AiChatResponse, AiTip } from "@/types";
export const coachingService = {
async chat(data: AiChatRequest): Promise<AiChatResponse> {
return api.post<AiChatResponse>("/coach/chat", data);
},
async getHint(data: { topic_id: number; question_id: string }): Promise<{ hint: string }> {
return api.post("/coach/hint", data);
},
async explain(data: { context: string; scores?: Record<string, number> }): Promise<{ explanation: string }> {
return api.post("/coach/explain", data);
},
async suggest(data?: { subject_id?: number }): Promise<{ suggestions: string[]; study_plan_tips: string[] }> {
return api.post("/coach/suggest", data);
},
async writingHelp(data: { text: string; task_type: string }): Promise<{ feedback: string; improved: string; grammar_notes: string[] }> {
return api.post("/coach/writing-help", data);
},
async getTip(context: string): Promise<AiTip> {
return api.get<AiTip>("/coach/tip", { context });
},
};

View File

@@ -0,0 +1,96 @@
import { api } from "@/lib/api-client";
import type {
DiscussionBoard,
DiscussionPost,
Announcement,
Message,
PostCreateRequest,
AnnouncementCreateRequest,
MessageCreateRequest,
} from "@/types/communication";
import type { ApiSuccessResponse, PaginatedResponse, PaginationParams } from "@/types";
export const communicationService = {
async listDiscussionBoards(params?: { course_id?: number; batch_id?: number }): Promise<DiscussionBoard[]> {
return api.get<DiscussionBoard[]>("/discussion-boards", params as Record<string, string | number | boolean | undefined>);
},
async createDiscussionBoard(data: Partial<DiscussionBoard> & Pick<DiscussionBoard, "name" | "course_id">): Promise<DiscussionBoard> {
return api.post<DiscussionBoard>("/discussion-boards", data);
},
async updateDiscussionBoard(id: number, data: Partial<DiscussionBoard>): Promise<DiscussionBoard> {
return api.patch<DiscussionBoard>(`/discussion-boards/${id}`, data);
},
async listPosts(boardId: number, params?: PaginationParams): Promise<PaginatedResponse<DiscussionPost>> {
return api.get<PaginatedResponse<DiscussionPost>>(
`/discussion-boards/${boardId}/posts`,
params as Record<string, string | number | boolean | undefined>,
);
},
async createPost(boardId: number, data: PostCreateRequest): Promise<DiscussionPost> {
return api.post<DiscussionPost>(`/discussion-boards/${boardId}/posts`, data);
},
async updatePost(id: number, data: Partial<PostCreateRequest>): Promise<DiscussionPost> {
return api.patch<DiscussionPost>(`/posts/${id}`, data);
},
async deletePost(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/posts/${id}`);
},
async pinPost(id: number, body?: { pinned?: boolean }): Promise<DiscussionPost> {
return api.post<DiscussionPost>(`/posts/${id}/pin`, body);
},
async resolvePost(id: number): Promise<DiscussionPost> {
return api.post<DiscussionPost>(`/posts/${id}/resolve`);
},
async listAnnouncements(params?: { course_id?: number; priority?: string }): Promise<Announcement[]> {
return api.get<Announcement[]>("/announcements", params as Record<string, string | number | boolean | undefined>);
},
async createAnnouncement(data: AnnouncementCreateRequest): Promise<Announcement> {
return api.post<Announcement>("/announcements", data);
},
async updateAnnouncement(id: number, data: Partial<AnnouncementCreateRequest>): Promise<Announcement> {
return api.patch<Announcement>(`/announcements/${id}`, data);
},
async deleteAnnouncement(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/announcements/${id}`);
},
async publishAnnouncement(id: number): Promise<Announcement> {
return api.post<Announcement>(`/announcements/${id}/publish`);
},
async listMessages(params?: { is_read?: boolean } & PaginationParams): Promise<PaginatedResponse<Message>> {
return api.get<PaginatedResponse<Message>>("/messages", params as Record<string, string | number | boolean | undefined>);
},
async listSentMessages(params?: PaginationParams): Promise<PaginatedResponse<Message>> {
return api.get<PaginatedResponse<Message>>("/messages/sent", params as Record<string, string | number | boolean | undefined>);
},
async sendMessage(data: MessageCreateRequest): Promise<Message> {
return api.post<Message>("/messages", data);
},
async getMessage(id: number): Promise<Message> {
return api.get<Message>(`/messages/${id}`);
},
async deleteMessage(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/messages/${id}`);
},
async getUnreadMessageCount(): Promise<{ count: number }> {
return api.get<{ count: number }>("/messages/unread-count");
},
};

View File

@@ -0,0 +1,37 @@
import { api } from "@/lib/api-client";
import type {
GapAnalysis,
CourseConfig,
CourseProgress,
CheckpointResult,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const courseGenerationService = {
getGapAnalysis: (source: "exam" | "placement", sourceId: number) =>
api.get<GapAnalysis>("/course/gap-analysis", { source, source_id: sourceId }),
autoGenerate: (sourceId: number) =>
api.post<CourseConfig>("/course/auto-generate", { source_id: sourceId }),
create: (data: Partial<CourseConfig>) =>
api.post<CourseConfig>("/course/create", data),
getCourse: (courseId: number) =>
api.get<CourseConfig>(`/course/${courseId}`),
updateCourse: (courseId: number, data: Partial<CourseConfig>) =>
api.put<ApiSuccessResponse>(`/course/${courseId}`, data),
trackProgress: (courseId: number, data: CourseProgress) =>
api.post<ApiSuccessResponse>(`/course/${courseId}/progress`, data),
submitCheckpoint: (courseId: number, data: { module_id: number; answers: Record<string, string> }) =>
api.post<CheckpointResult>(`/course/${courseId}/checkpoint`, data),
startPostTest: (courseId: number) =>
api.post<{ exam_id: number }>(`/course/${courseId}/post-test`),
generateAIResource: (data: { topic: string; difficulty: string; type: string }) =>
api.post<{ resource_id: number }>("/ai/generate-resource", data),
};

View File

@@ -0,0 +1,113 @@
import { api, API_BASE_URL } from "@/lib/api-client";
import type {
CourseChapter,
ChapterMaterial,
ChapterProgress,
ChapterCreateRequest,
WorkbenchGenerateRequest,
WorkbenchGeneratedOutline,
WorkbenchGeneratedChapter,
} from "@/types/courseware";
import type { ApiSuccessResponse } from "@/types";
const blobDownloadHeaders = (): Record<string, string> => ({
Authorization: `Bearer ${localStorage.getItem("encoach_token") ?? ""}`,
});
export const coursewareService = {
async listChapters(courseId: number): Promise<CourseChapter[]> {
return api.get<CourseChapter[]>(`/courses/${courseId}/chapters`);
},
async createChapter(courseId: number, data: ChapterCreateRequest): Promise<CourseChapter> {
return api.post<CourseChapter>(`/courses/${courseId}/chapters`, data);
},
async getChapter(id: number): Promise<CourseChapter> {
return api.get<CourseChapter>(`/chapters/${id}`);
},
async updateChapter(id: number, data: Partial<ChapterCreateRequest>): Promise<CourseChapter> {
return api.patch<CourseChapter>(`/chapters/${id}`, data);
},
async deleteChapter(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/chapters/${id}`);
},
async unlockChapter(id: number): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/chapters/${id}/unlock`);
},
async lockChapter(id: number): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/chapters/${id}/lock`);
},
async reorderChapters(courseId: number, body: { ids: number[] }): Promise<ApiSuccessResponse> {
return api.patch<ApiSuccessResponse>(`/courses/${courseId}/chapters/reorder`, body);
},
async getChapterProgress(id: number): Promise<ChapterProgress> {
return api.get<ChapterProgress>(`/chapters/${id}/progress`);
},
async markChapterComplete(id: number): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/chapters/${id}/progress/complete`);
},
async listMaterials(chapterId: number): Promise<ChapterMaterial[]> {
return api.get<ChapterMaterial[]>(`/chapters/${chapterId}/materials`);
},
async uploadMaterial(chapterId: number, formData: FormData): Promise<ChapterMaterial> {
return api.upload<ChapterMaterial>(`/chapters/${chapterId}/materials`, formData);
},
async updateMaterial(id: number, data: Partial<ChapterMaterial>): Promise<ChapterMaterial> {
return api.patch<ChapterMaterial>(`/materials/${id}`, data);
},
async deleteMaterial(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/materials/${id}`);
},
async downloadMaterial(id: number): Promise<Blob> {
const res = await fetch(`${API_BASE_URL}/materials/${id}/download`, {
headers: blobDownloadHeaders(),
});
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
return res.blob();
},
async markMaterialViewed(id: number): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/materials/${id}/viewed`);
},
async reorderMaterials(chapterId: number, body: { ids: number[] }): Promise<ApiSuccessResponse> {
return api.patch<ApiSuccessResponse>(`/chapters/${chapterId}/materials/reorder`, body);
},
async generateOutline(data: WorkbenchGenerateRequest): Promise<WorkbenchGeneratedOutline> {
return api.post<WorkbenchGeneratedOutline>("/workbench/generate-outline", data);
},
async generateChapterContent(data: WorkbenchGenerateRequest): Promise<WorkbenchGeneratedChapter> {
return api.post<WorkbenchGeneratedChapter>("/workbench/generate-chapter", data);
},
async generateRubric(data: WorkbenchGenerateRequest): Promise<{ rubric: string }> {
return api.post("/workbench/generate-rubric", data);
},
async regenerateSection(data: Record<string, unknown>): Promise<WorkbenchGeneratedChapter> {
return api.post<WorkbenchGeneratedChapter>("/workbench/regenerate", data);
},
async suggestMaterials(data: Record<string, unknown>): Promise<unknown> {
return api.post("/workbench/suggest-materials", data);
},
async publishWorkbench(data: Record<string, unknown>): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>("/workbench/publish", data);
},
};

View File

@@ -0,0 +1,17 @@
import { api } from "@/lib/api-client";
import type { CustomExamCreateRequest, CustomExamConfig } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const customExamService = {
create: (data: CustomExamCreateRequest) =>
api.post<{ exam_id: number }>("/exam/custom/create", data),
getById: (examId: number) =>
api.get<CustomExamConfig>(`/exam/custom/${examId}`),
update: (examId: number, data: Partial<CustomExamConfig>) =>
api.put<ApiSuccessResponse>(`/exam/custom/${examId}`, data),
delete: (examId: number) =>
api.delete<ApiSuccessResponse>(`/exam/custom/${examId}`),
};

View File

@@ -0,0 +1,45 @@
import { api } from "@/lib/api-client";
import { asPaginated, asRecordData } from "@/lib/odoo-api";
import type { Entity, EntityRole, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const entitiesService = {
async list(params?: PaginationParams): Promise<PaginatedResponse<Entity>> {
const raw = await api.get<unknown>("/entities", params as Record<string, string | number | boolean | undefined>);
return asPaginated<Entity>(raw);
},
async getById(id: number): Promise<Entity> {
const raw = await api.get<unknown>(`/entities/${id}`);
return asRecordData<Entity>(raw);
},
async create(data: Partial<Entity>): Promise<Entity> {
const raw = await api.post<unknown>("/entities", data);
return asRecordData<Entity>(raw);
},
async update(id: number, data: Partial<Entity>): Promise<Entity> {
const raw = await api.patch<unknown>(`/entities/${id}`, data);
return asRecordData<Entity>(raw);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/entities/${id}`);
},
async getRoles(entityId: number): Promise<EntityRole[]> {
return api.get<EntityRole[]>(`/entities/${entityId}/roles`);
},
async createRole(entityId: number, data: Partial<EntityRole>): Promise<EntityRole> {
return api.post<EntityRole>(`/entities/${entityId}/roles`, data);
},
async updateRolePermissions(roleId: number, permissions: string[]): Promise<EntityRole> {
return api.patch<EntityRole>(`/roles/${roleId}/permissions`, { permissions });
},
async getPermissions(entityId: number): Promise<string[]> {
return api.get<string[]>(`/permissions`, { entity_id: entityId });
},
};

View File

@@ -0,0 +1,36 @@
import { api } from "@/lib/api-client";
import type {
BulkCreateResult,
CSVValidationReport,
CredentialFilters,
CredentialStatus,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const entityOnboardingService = {
validateCsv: (file: File) => {
const fd = new FormData();
fd.append("file", file);
return api.upload<CSVValidationReport>("/entity/students/csv/validate", fd);
},
bulkCreate: (payload: { validate_session_id?: string }) =>
api.post<BulkCreateResult>("/entity/students/bulk-create", payload),
sendCredentials: (studentIds: number[]) =>
api.post<ApiSuccessResponse>("/entity/students/send-credentials", { student_ids: studentIds }),
getCredentialStatuses: (
filters?: CredentialFilters & { page?: number; limit?: number },
) =>
api.get<{ data: CredentialStatus[]; pagination?: { total: number; page: number } }>(
"/entity/students/credentials",
filters as Record<string, string | number | boolean | undefined>,
),
resendCredential: (studentId: number) =>
api.post<ApiSuccessResponse>(`/entity/students/${studentId}/resend-credential`),
resendAllPending: () =>
api.post<ApiSuccessResponse>("/entity/students/resend-credentials/pending"),
};

View File

@@ -0,0 +1,20 @@
import { api } from "@/lib/api-client";
import type { Evaluation } from "@/types";
export const evaluationsService = {
async evaluateWriting(data: { session_id: number; text: string; rubric_id?: number }): Promise<Evaluation> {
return api.post<Evaluation>("/evaluate/writing", data);
},
async evaluateSpeaking(data: { session_id: number; audio_url: string; rubric_id?: number }): Promise<Evaluation> {
return api.post<Evaluation>("/evaluate/speaking", data);
},
async gradeMultiple(data: { session_id: number; answers: { exercise_index: number; answer: string }[] }): Promise<{ results: { exercise_index: number; correct: boolean; score: number }[] }> {
return api.post("/grading/multiple", data);
},
async transcribe(formData: FormData): Promise<{ text: string }> {
return api.upload<{ text: string }>("/transcribe", formData);
},
};

View File

@@ -0,0 +1,17 @@
import { api } from "@/lib/api-client";
import type { ExamSession, ExamAutoSave, ExamSubmitResponse } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const examSessionService = {
getSession: (examId: number) =>
api.get<ExamSession>(`/exam/${examId}/session`),
autoSave: (examId: number, data: ExamAutoSave) =>
api.post<ApiSuccessResponse>(`/exam/${examId}/autosave`, data),
submit: (examId: number) =>
api.post<ExamSubmitResponse>(`/exam/${examId}/submit`),
getStatus: (examId: number) =>
api.get<{ status: string; scores_available: boolean }>(`/exam/${examId}/status`),
};

View File

@@ -0,0 +1,17 @@
import { api } from "@/lib/api-client";
import type { ExamTemplate, SaveAsTemplateRequest } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const examTemplateService = {
listInternational: () =>
api.get<ExamTemplate[]>("/exam/templates", { type: "international" }),
listCustom: () =>
api.get<ExamTemplate[]>("/exam/templates", { type: "custom" }),
getById: (id: number) =>
api.get<ExamTemplate>(`/exam/templates/${id}`),
saveCustomTemplate: (data: SaveAsTemplateRequest) =>
api.post<ApiSuccessResponse>("/exam/templates/custom", data),
};

View File

@@ -0,0 +1,62 @@
import { api } from "@/lib/api-client";
import type { Exam, ExamModule, Rubric, RubricGroup, ExamStructure, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export interface ExamListParams extends PaginationParams {
module?: ExamModule;
subject_id?: number;
entity_id?: number;
}
export const examsService = {
async list(module: ExamModule, params?: PaginationParams): Promise<PaginatedResponse<Exam>> {
return api.get<PaginatedResponse<Exam>>(`/exam/${module}`, params as Record<string, string | number | boolean | undefined>);
},
async getById(module: ExamModule, id: number): Promise<Exam> {
return api.get<Exam>(`/exam/${module}/${id}`);
},
async create(data: Partial<Exam>): Promise<Exam> {
return api.post<Exam>("/exam", data);
},
async update(id: number, data: Partial<Exam>): Promise<Exam> {
return api.patch<Exam>(`/exam/${id}`, data);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/exam/${id}`);
},
async setAccess(id: number, access: "public" | "private"): Promise<Exam> {
return api.patch<Exam>(`/exam/${id}/access`, { access });
},
async listRubrics(params?: PaginationParams): Promise<PaginatedResponse<Rubric>> {
return api.get<PaginatedResponse<Rubric>>("/rubrics", params as Record<string, string | number | boolean | undefined>);
},
async createRubric(data: Partial<Rubric>): Promise<Rubric> {
return api.post<Rubric>("/rubrics", data);
},
async listRubricGroups(params?: PaginationParams): Promise<PaginatedResponse<RubricGroup>> {
return api.get<PaginatedResponse<RubricGroup>>("/rubric-groups", params as Record<string, string | number | boolean | undefined>);
},
async listStructures(params?: PaginationParams & { entity_id?: number }): Promise<PaginatedResponse<ExamStructure>> {
return api.get<PaginatedResponse<ExamStructure>>("/exam-structures", params as Record<string, string | number | boolean | undefined>);
},
async createStructure(data: Partial<ExamStructure>): Promise<ExamStructure> {
return api.post<ExamStructure>("/exam-structures", data);
},
async deleteStructure(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/exam-structures/${id}`);
},
async getAvatars(): Promise<{ id: number; name: string; thumbnail: string }[]> {
return api.get("/exam/avatars");
},
};

View File

@@ -0,0 +1,33 @@
import { api } from "@/lib/api-client";
import type { Facility, Asset, FacilityCreateRequest, AssetCreateRequest } from "@/types/facility";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const facilityService = {
async listFacilities(params?: PaginationParams): Promise<PaginatedResponse<Facility>> {
return api.get<PaginatedResponse<Facility>>("/facilities", params as Record<string, string | number | boolean | undefined>);
},
async createFacility(data: FacilityCreateRequest): Promise<Facility> {
return api.post<Facility>("/facilities", data);
},
async updateFacility(id: number, data: Partial<FacilityCreateRequest>): Promise<Facility> {
return api.patch<Facility>(`/facilities/${id}`, data);
},
async deleteFacility(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/facilities/${id}`);
},
async listAssets(params?: PaginationParams): Promise<PaginatedResponse<Asset>> {
return api.get<PaginatedResponse<Asset>>("/assets", params as Record<string, string | number | boolean | undefined>);
},
async createAsset(data: AssetCreateRequest): Promise<Asset> {
return api.post<Asset>("/assets", data);
},
async deleteAsset(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/assets/${id}`);
},
};

View File

@@ -0,0 +1,42 @@
import { api } from "@/lib/api-client";
import type {
FaqCategory,
FaqItem,
FaqCategoryCreateRequest,
FaqItemCreateRequest,
} from "@/types/faq";
import type { ApiSuccessResponse } from "@/types";
export const faqService = {
async listCategories(params?: { audience?: string }): Promise<FaqCategory[]> {
return api.get<FaqCategory[]>("/faq/categories", params as Record<string, string | number | boolean | undefined>);
},
async createCategory(data: FaqCategoryCreateRequest): Promise<FaqCategory> {
return api.post<FaqCategory>("/faq/categories", data);
},
async updateCategory(id: number, data: Partial<FaqCategoryCreateRequest>): Promise<FaqCategory> {
return api.patch<FaqCategory>(`/faq/categories/${id}`, data);
},
async deleteCategory(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/faq/categories/${id}`);
},
async listItems(params?: { category_id?: number; audience?: string; search?: string }): Promise<FaqItem[]> {
return api.get<FaqItem[]>("/faq/items", params as Record<string, string | number | boolean | undefined>);
},
async createItem(data: FaqItemCreateRequest): Promise<FaqItem> {
return api.post<FaqItem>("/faq/items", data);
},
async updateItem(id: number, data: Partial<FaqItemCreateRequest>): Promise<FaqItem> {
return api.patch<FaqItem>(`/faq/items/${id}`, data);
},
async deleteItem(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/faq/items/${id}`);
},
};

View File

@@ -0,0 +1,21 @@
import { api } from "@/lib/api-client";
import type { FeesPlan, StudentFeesDetail, FeesTerms } from "@/types/fees";
import type { PaginatedResponse, PaginationParams } from "@/types";
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 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 listFeesTerms(params?: PaginationParams): Promise<PaginatedResponse<FeesTerms>> {
return api.get<PaginatedResponse<FeesTerms>>("/fees-terms", params as Record<string, string | number | boolean | undefined>);
},
};

View File

@@ -0,0 +1,22 @@
import { api } from "@/lib/api-client";
import type { ExamModule } from "@/types";
export interface GenerationParams {
title: string;
label?: string;
entity_id?: number;
subject_id?: number;
topic_id?: number;
difficulty?: string;
count?: number;
}
export const generationService = {
async generate(module: ExamModule, params: GenerationParams): Promise<{ exam_id: number; exercises: unknown[] }> {
return api.post(`/exam/${module}/generate`, params);
},
async generateFromScratch(module: ExamModule, params: GenerationParams): Promise<{ exam_id: number; exercises: unknown[] }> {
return api.post(`/exam/${module}/generate/scratch`, params);
},
};

View File

@@ -0,0 +1,29 @@
import { api } from "@/lib/api-client";
import type { Gradebook, GradebookLine, GradingAssignment, GradingAssignmentCreateRequest } from "@/types/gradebook";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const gradebookService = {
async listGradebooks(params?: PaginationParams): Promise<PaginatedResponse<Gradebook>> {
return api.get<PaginatedResponse<Gradebook>>("/gradebooks", params as Record<string, string | number | boolean | undefined>);
},
async listGradebookLines(params?: PaginationParams): Promise<PaginatedResponse<GradebookLine>> {
return api.get<PaginatedResponse<GradebookLine>>("/gradebook-lines", params as Record<string, string | number | boolean | undefined>);
},
async listGradingAssignments(params?: PaginationParams): Promise<PaginatedResponse<GradingAssignment>> {
return api.get<PaginatedResponse<GradingAssignment>>("/grading-assignments", params as Record<string, string | number | boolean | undefined>);
},
async createGradingAssignment(data: GradingAssignmentCreateRequest): Promise<GradingAssignment> {
return api.post<GradingAssignment>("/grading-assignments", data);
},
async updateGradingAssignment(id: number, data: Partial<GradingAssignmentCreateRequest>): Promise<GradingAssignment> {
return api.patch<GradingAssignment>(`/grading-assignments/${id}`, data);
},
async deleteGradingAssignment(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/grading-assignments/${id}`);
},
};

View File

@@ -0,0 +1,26 @@
import { api } from "@/lib/api-client";
import type {
GradingQueueItem,
StudentResponse,
RubricCriteria,
AIGradeSuggestion,
GradeSubmission,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const gradingService = {
getQueue: (params?: { skill?: string; status?: string; exam_id?: number }) =>
api.get<GradingQueueItem[]>("/grading/queue", params as Record<string, string | number | boolean | undefined>),
getStudentResponse: (attemptId: number, skill: string) =>
api.get<StudentResponse>(`/grading/${attemptId}/response`, { skill }),
getRubric: (attemptId: number, skill: string) =>
api.get<RubricCriteria[]>(`/grading/${attemptId}/rubric`, { skill }),
getAISuggestion: (attemptId: number, skill: string) =>
api.post<AIGradeSuggestion>("/grading/ai-suggest", { attempt_id: attemptId, skill }),
submitGrade: (data: GradeSubmission) =>
api.post<ApiSuccessResponse>(`/grading/${data.attempt_id}/submit`, data),
};

View File

@@ -0,0 +1,44 @@
import { api } from "@/lib/api-client";
import type {
IELTSExamConfig,
IELTSExamResponse,
IELTSSkillConfig,
ContentPoolItem,
ContentPoolFilters,
AssemblyResult,
ExamValidationReport,
ExamAssignRequest,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const ieltsExamService = {
create: (data: IELTSExamConfig) =>
api.post<IELTSExamResponse>("/exam/ielts/create", data),
getSkills: (examId: number) =>
api.get<IELTSSkillConfig[]>(`/exam/ielts/${examId}/skills`),
getContentPoolCount: (examId: number) =>
api.get<Record<string, number>>(`/exam/ielts/${examId}/content-pool-count`),
getContentPool: (examId: number, filters: ContentPoolFilters) =>
api.get<ContentPoolItem[]>(`/exam/ielts/${examId}/content-pool`, filters as Record<string, string | number | boolean | undefined>),
autoAssemble: (examId: number) =>
api.post<AssemblyResult[]>(`/exam/ielts/${examId}/auto-assemble`),
suggest: (examId: number) =>
api.post<AssemblyResult[]>(`/exam/ielts/${examId}/suggest`),
saveQuestions: (examId: number, sectionId: number, questionIds: number[]) =>
api.put<ApiSuccessResponse>(`/exam/ielts/${examId}/sections/${sectionId}/questions`, { question_ids: questionIds }),
validate: (examId: number) =>
api.get<ExamValidationReport>(`/exam/ielts/${examId}/validate`),
update: (examId: number, data: { status: string }) =>
api.put<ApiSuccessResponse>(`/exam/ielts/${examId}`, data),
assign: (examId: number, data: ExamAssignRequest) =>
api.post<ApiSuccessResponse>(`/exam/ielts/${examId}/assign`, data),
};

View File

@@ -0,0 +1,37 @@
export { authService } from "./auth.service";
export { usersService } from "./users.service";
export { entitiesService } from "./entities.service";
export { examsService } from "./exams.service";
export { assignmentsService } from "./assignments.service";
export { classroomsService } from "./classrooms.service";
export { statsService } from "./stats.service";
export { evaluationsService } from "./evaluations.service";
export { generationService } from "./generation.service";
export { mediaService } from "./media.service";
export { trainingService } from "./training.service";
export { subscriptionsService } from "./subscriptions.service";
export { ticketsService } from "./tickets.service";
export { storageService } from "./storage.service";
export { approvalsService } from "./approvals.service";
export { taxonomyService } from "./taxonomy.service";
export { adaptiveService } from "./adaptive.service";
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";
export { coursewareService } from "./courseware.service";
export { communicationService } from "./communication.service";
export { notificationService } from "./notification.service";
export { faqService } from "./faq.service";
export { plagiarismService } from "./plagiarism.service";
export { studentLeaveService } from "./student-leave.service";
export { feesService } from "./fees.service";
export { lessonService } from "./lesson.service";
export { gradebookService } from "./gradebook.service";
export { studentProgressService } from "./student-progress.service";
export { libraryService } from "./library.service";
export { activityService } from "./activity.service";
export { facilityService } from "./facility.service";

View File

@@ -0,0 +1,167 @@
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<PaginatedResponse<InstExamSession>> {
return api.get<PaginatedResponse<InstExamSession>>("/inst-exam-sessions", params as Record<string, string | number | boolean | undefined>);
},
async getSession(id: number): Promise<InstExamSession> {
return api.get<InstExamSession>(`/inst-exam-sessions/${id}`);
},
async createSession(data: InstExamSessionCreateRequest): Promise<InstExamSession> {
return api.post<InstExamSession>("/inst-exam-sessions", data);
},
async updateSession(id: number, data: Partial<InstExamSessionCreateRequest>): Promise<InstExamSession> {
return api.patch<InstExamSession>(`/inst-exam-sessions/${id}`, data);
},
async deleteSession(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/inst-exam-sessions/${id}`);
},
async scheduleSession(id: number): Promise<InstExamSession> {
return api.post<InstExamSession>(`/inst-exam-sessions/${id}/schedule`);
},
async markSessionDone(id: number): Promise<InstExamSession> {
return api.post<InstExamSession>(`/inst-exam-sessions/${id}/done`);
},
async createExam(data: InstExamCreateRequest): Promise<InstExam> {
return api.post<InstExam>("/inst-exams", data);
},
async updateExam(id: number, data: Partial<InstExamCreateRequest>): Promise<InstExam> {
return api.patch<InstExam>(`/inst-exams/${id}`, data);
},
async addAttendees(examId: number, data: { student_ids: number[] }): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/inst-exams/${examId}/attendees`, data);
},
async enterMarks(examId: number, data: { marks: { student_id: number; score: number }[] }): Promise<ApiSuccessResponse> {
return api.patch<ApiSuccessResponse>(`/inst-exams/${examId}/marks`, data);
},
async listExamTypes(params?: PaginationParams): Promise<PaginatedResponse<ExamType>> {
return api.get<PaginatedResponse<ExamType>>("/exam-types", params as Record<string, string | number | boolean | undefined>);
},
async createExamType(data: ExamTypeCreateRequest): Promise<ExamType> {
return api.post<ExamType>("/exam-types", data);
},
async listGradeConfigs(params?: PaginationParams): Promise<PaginatedResponse<GradeConfig>> {
return api.get<PaginatedResponse<GradeConfig>>("/grade-config", params as Record<string, string | number | boolean | undefined>);
},
async createGradeConfig(data: GradeConfigCreateRequest): Promise<GradeConfig> {
return api.post<GradeConfig>("/grade-config", data);
},
async listResultTemplates(params?: PaginationParams): Promise<PaginatedResponse<ResultTemplate>> {
return api.get<PaginatedResponse<ResultTemplate>>("/result-templates", params as Record<string, string | number | boolean | undefined>);
},
async createResultTemplate(data: ResultTemplateCreateRequest): Promise<ResultTemplate> {
return api.post<ResultTemplate>("/result-templates", data);
},
async generateMarksheets(templateId: number): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/result-templates/${templateId}/generate`);
},
async listMarksheets(params?: PaginationParams): Promise<PaginatedResponse<Marksheet>> {
return api.get<PaginatedResponse<Marksheet>>("/marksheets", params as Record<string, string | number | boolean | undefined>);
},
async getMarksheet(id: number): Promise<Marksheet> {
return api.get<Marksheet>(`/marksheets/${id}`);
},
async validateMarksheet(id: number): Promise<Marksheet> {
return api.post<Marksheet>(`/marksheets/${id}/validate`);
},
async listCourseAssignments(params?: PaginationParams & { course_id?: number; batch_id?: number; state?: string }): Promise<PaginatedResponse<CourseAssignment>> {
return api.get<PaginatedResponse<CourseAssignment>>("/course-assignments", params as Record<string, string | number | boolean | undefined>);
},
async createCourseAssignment(data: CourseAssignmentCreateRequest): Promise<CourseAssignment> {
return api.post<CourseAssignment>("/course-assignments", data);
},
async getCourseAssignment(id: number): Promise<CourseAssignment> {
return api.get<CourseAssignment>(`/course-assignments/${id}`);
},
async updateCourseAssignment(id: number, data: Partial<CourseAssignmentCreateRequest>): Promise<CourseAssignment> {
return api.patch<CourseAssignment>(`/course-assignments/${id}`, data);
},
async publishCourseAssignment(id: number): Promise<CourseAssignment> {
return api.post<CourseAssignment>(`/course-assignments/${id}/publish`);
},
async finishCourseAssignment(id: number): Promise<CourseAssignment> {
return api.post<CourseAssignment>(`/course-assignments/${id}/finish`);
},
async listSubmissions(assignmentId: number, params?: PaginationParams): Promise<PaginatedResponse<AssignmentSubmission>> {
return api.get<PaginatedResponse<AssignmentSubmission>>(`/course-assignments/${assignmentId}/submissions`, params as Record<string, string | number | boolean | undefined>);
},
async submitAssignment(assignmentId: number, file: File): Promise<AssignmentSubmission> {
const formData = new FormData();
formData.append("file", file);
return api.upload<AssignmentSubmission>(`/course-assignments/${assignmentId}/submit`, formData);
},
async gradeSubmission(submissionId: number, data: Partial<AssignmentSubmission>): Promise<AssignmentSubmission> {
return api.patch<AssignmentSubmission>(`/submissions/${submissionId}`, data);
},
async listAssignmentTypes(params?: PaginationParams): Promise<PaginatedResponse<AssignmentType>> {
return api.get<PaginatedResponse<AssignmentType>>("/assignment-types", params as Record<string, string | number | boolean | undefined>);
},
async createAssignmentType(data: AssignmentTypeCreateRequest): Promise<AssignmentType> {
return api.post<AssignmentType>("/assignment-types", data);
},
async listSubjectRegistrations(params?: PaginationParams & { student_id?: number; course_id?: number; state?: string }): Promise<PaginatedResponse<SubjectRegistration>> {
return api.get<PaginatedResponse<SubjectRegistration>>("/subject-registrations", params as Record<string, string | number | boolean | undefined>);
},
async createSubjectRegistration(data: SubjectRegistrationCreateRequest): Promise<SubjectRegistration> {
return api.post<SubjectRegistration>("/subject-registrations", data);
},
async updateSubjectRegistration(id: number, data: Partial<SubjectRegistrationCreateRequest>): Promise<SubjectRegistration> {
return api.patch<SubjectRegistration>(`/subject-registrations/${id}`, data);
},
async confirmSubjectRegistration(id: number): Promise<SubjectRegistration> {
return api.post<SubjectRegistration>(`/subject-registrations/${id}/confirm`);
},
async rejectSubjectRegistration(id: number): Promise<SubjectRegistration> {
return api.post<SubjectRegistration>(`/subject-registrations/${id}/reject`);
},
async getAvailableSubjects(params?: { student_id?: number; course_id?: number }): Promise<SubjectRegistration[]> {
return api.get<SubjectRegistration[]>("/subject-registrations/available", params as Record<string, string | number | boolean | undefined>);
},
};

View File

@@ -0,0 +1,21 @@
import { api } from "@/lib/api-client";
import type { Lesson, LessonCreateRequest } from "@/types/lesson";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const lessonService = {
async listLessons(params?: PaginationParams): Promise<PaginatedResponse<Lesson>> {
return api.get<PaginatedResponse<Lesson>>("/lessons", params as Record<string, string | number | boolean | undefined>);
},
async createLesson(data: LessonCreateRequest): Promise<Lesson> {
return api.post<Lesson>("/lessons", data);
},
async updateLesson(id: number, data: Partial<LessonCreateRequest>): Promise<Lesson> {
return api.patch<Lesson>(`/lessons/${id}`, data);
},
async deleteLesson(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/lessons/${id}`);
},
};

View File

@@ -0,0 +1,13 @@
import { api } from "@/lib/api-client";
import type { LevelMapping, LevelMappingRow } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const levelMappingService = {
get: (entityId: number) => api.get<LevelMapping>(`/entity/${entityId}/level-mapping`),
update: (entityId: number, mappings: LevelMappingRow[]) =>
api.put<ApiSuccessResponse>(`/entity/${entityId}/level-mapping`, { mappings }),
resetToDefault: (entityId: number) =>
api.post<ApiSuccessResponse>(`/entity/${entityId}/level-mapping/reset`),
};

View File

@@ -0,0 +1,41 @@
import { api } from "@/lib/api-client";
import type { MediaItem, MediaMovement, LibraryCard, MediaCreateRequest } from "@/types/library";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const libraryService = {
async listMedia(params?: PaginationParams): Promise<PaginatedResponse<MediaItem>> {
return api.get<PaginatedResponse<MediaItem>>("/library/media", params as Record<string, string | number | boolean | undefined>);
},
async createMedia(data: MediaCreateRequest): Promise<MediaItem> {
return api.post<MediaItem>("/library/media", data);
},
async listMovements(params?: PaginationParams): Promise<PaginatedResponse<MediaMovement>> {
return api.get<PaginatedResponse<MediaMovement>>("/library/movements", params as Record<string, string | number | boolean | undefined>);
},
async createMovement(data: Partial<Omit<MediaMovement, "id" | "media_name" | "student_name" | "faculty_name">>): Promise<MediaMovement> {
return api.post<MediaMovement>("/library/movements", data);
},
async updateMovement(id: number, data: Partial<Omit<MediaMovement, "id" | "media_name" | "student_name" | "faculty_name">>): Promise<MediaMovement> {
return api.patch<MediaMovement>(`/library/movements/${id}`, data);
},
async deleteMedia(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/library/media/${id}`);
},
async returnMovement(id: number): Promise<MediaMovement> {
return api.post<MediaMovement>(`/library/movements/${id}/return`);
},
async listCards(params?: PaginationParams): Promise<PaginatedResponse<LibraryCard>> {
return api.get<PaginatedResponse<LibraryCard>>("/library/cards", params as Record<string, string | number | boolean | undefined>);
},
async createCard(data: { student_id: number }): Promise<LibraryCard> {
return api.post<LibraryCard>("/library/cards", data);
},
};

View File

@@ -0,0 +1,225 @@
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 ?? [];
},
};

View File

@@ -0,0 +1,15 @@
import { api } from "@/lib/api-client";
export const mediaService = {
async generateListeningAudio(data: { text: string; voice_id?: string }): Promise<{ audio_url: string }> {
return api.post("/exam/listening/media", data);
},
async generateSpeakingVideo(data: { text: string; avatar_id?: number }): Promise<{ video_url: string; job_id: string }> {
return api.post("/exam/speaking/media", data);
},
async getAvatars(): Promise<{ id: number; name: string; thumbnail: string; voice: string }[]> {
return api.get("/exam/avatars");
},
};

View File

@@ -0,0 +1,55 @@
import { api } from "@/lib/api-client";
import type {
Notification,
NotificationRule,
NotificationPreferences,
NotificationRuleCreateRequest,
} from "@/types/notification";
import type { ApiSuccessResponse, PaginatedResponse, PaginationParams } from "@/types";
export const notificationService = {
async listNotifications(
params?: PaginationParams & { type?: string; is_read?: boolean },
): Promise<PaginatedResponse<Notification>> {
return api.get<PaginatedResponse<Notification>>(
"/notifications",
params as Record<string, string | number | boolean | undefined>,
);
},
async markNotificationRead(id: number): Promise<Notification> {
return api.post<Notification>(`/notifications/${id}/read`);
},
async markAllNotificationsRead(): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>("/notifications/read-all");
},
async getUnreadNotificationCount(): Promise<{ count: number }> {
return api.get<{ count: number }>("/notifications/unread-count");
},
async listNotificationRules(): Promise<NotificationRule[]> {
return api.get<NotificationRule[]>("/notification-rules");
},
async createNotificationRule(data: NotificationRuleCreateRequest): Promise<NotificationRule> {
return api.post<NotificationRule>("/notification-rules", data);
},
async updateNotificationRule(id: number, data: Partial<NotificationRuleCreateRequest>): Promise<NotificationRule> {
return api.patch<NotificationRule>(`/notification-rules/${id}`, data);
},
async deleteNotificationRule(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/notification-rules/${id}`);
},
async getNotificationPreferences(): Promise<NotificationPreferences> {
return api.get<NotificationPreferences>("/notification-preferences");
},
async updateNotificationPreferences(data: Partial<NotificationPreferences>): Promise<NotificationPreferences> {
return api.patch<NotificationPreferences>("/notification-preferences", data);
},
};

View File

@@ -0,0 +1,42 @@
import { api } from "@/lib/api-client";
import type {
PlacementSubject,
CATStartResponse,
CATAnswer,
CATAnswerResponse,
CATAutoSave,
PlacementResult,
LearningPathPreview,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const placementService = {
start: (subject: PlacementSubject) =>
api.post<CATStartResponse>("/placement/start", { subject }),
submitAnswer: (data: CATAnswer) =>
api.post<CATAnswerResponse>("/placement/answer", data),
autoSave: (data: CATAutoSave) =>
api.post<ApiSuccessResponse>("/placement/autosave", data),
uploadSpeaking: (sessionId: string, promptId: number, audioFile: File) => {
const formData = new FormData();
formData.append("session_id", sessionId);
formData.append("prompt_id", String(promptId));
formData.append("audio_file", audioFile);
return api.upload<ApiSuccessResponse>("/placement/speaking-upload", formData);
},
getResults: (sessionId: string) =>
api.get<PlacementResult>("/placement/results", { session_id: sessionId }),
getSpeakingStatus: (sessionId: string) =>
api.get<{ status: "pending" | "scored" }>("/placement/speaking-status", { session_id: sessionId }),
getLearningPath: (sessionId: string) =>
api.get<LearningPathPreview>("/placement/learning-path", { session_id: sessionId }),
startTrial: () =>
api.post<ApiSuccessResponse>("/subscription/trial"),
};

View File

@@ -0,0 +1,29 @@
import { api, API_BASE_URL } from "@/lib/api-client";
export interface PlagiarismCheckRequest {
text: string;
submission_id?: number;
}
export interface PlagiarismCheckResult {
similarity_score: number;
matches: { source: string; excerpt: string; score: number }[];
}
export const plagiarismService = {
async check(body: PlagiarismCheckRequest): Promise<PlagiarismCheckResult> {
return api.post<PlagiarismCheckResult>("/plagiarism/check", body);
},
async getReport(submissionId: number): Promise<PlagiarismCheckResult> {
return api.get<PlagiarismCheckResult>(`/plagiarism/report/${submissionId}`);
},
async downloadReport(submissionId: number): Promise<Blob> {
const res = await fetch(`${API_BASE_URL}/plagiarism/report/${submissionId}/download`, {
headers: { Authorization: `Bearer ${localStorage.getItem("encoach_token") ?? ""}` },
});
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
return res.blob();
},
};

View File

@@ -0,0 +1,19 @@
import { api } from "@/lib/api-client";
export const reportService = {
downloadPdf: async (attemptId: number): Promise<void> => {
const response = await fetch(`/api/reports/exam/${attemptId}/pdf`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("encoach_token")}`,
},
});
if (!response.ok) throw new Error("Failed to generate report");
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `exam-report-${attemptId}.pdf`;
link.click();
window.URL.revokeObjectURL(url);
},
};

View File

@@ -0,0 +1,48 @@
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";
export interface ResourceListParams extends PaginationParams {
topic_id?: number;
resource_type?: string;
review_status?: string;
search?: string;
}
export const resourcesService = {
async list(params?: ResourceListParams): Promise<PaginatedResponse<Resource>> {
const raw = await api.get<unknown>("/resources", params as Record<string, string | number | boolean | undefined>);
return asPaginated<Resource>(raw);
},
async upload(formData: FormData): Promise<Resource> {
const raw = await api.upload<unknown>("/resources", formData);
return asRecordData<Resource>(raw);
},
async update(id: number, data: Partial<Resource>): Promise<Resource> {
const raw = await api.patch<unknown>(`/resources/${id}`, data);
return asRecordData<Resource>(raw);
},
async delete(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/resources/${id}`);
},
async markComplete(id: number): Promise<ResourceCompletion> {
const raw = await api.post<unknown>(`/resources/${id}/complete`);
return asRecordData<ResourceCompletion>(raw);
},
async rate(id: number, rating: number): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>(`/resources/${id}/rate`, { rating });
},
async download(id: number): Promise<Blob> {
const res = await fetch(`${API_BASE_URL}/resources/${id}/download`, {
headers: { Authorization: `Bearer ${localStorage.getItem("encoach_token") ?? ""}` },
});
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
return res.blob();
},
};

View File

@@ -0,0 +1,75 @@
import { api } from "@/lib/api-client";
import type {
Role, Permission, RoleCreateRequest, PermissionCreateRequest,
AuthorityMatrixData, UserWithRoles, UserRoleDetail, UserAuthorityMatrixData,
} from "@/types/role-permission";
interface PaginatedRoles { data: Role[]; total: number; page: number; size: number }
interface PaginatedUsers { data: UserWithRoles[]; total: number; page: number; size: number }
interface PermissionList { data: Permission[]; total: number }
type Params = Record<string, string | number | boolean | undefined>;
export const rolesService = {
// ── Roles ──
async listRoles(params?: { search?: string; entity_id?: number }): Promise<PaginatedRoles> {
return api.get<PaginatedRoles>("/roles", params as Params);
},
async getRole(id: number): Promise<{ data: Role }> {
return api.get<{ data: Role }>(`/roles/${id}`);
},
async createRole(data: RoleCreateRequest): Promise<{ data: Role }> {
return api.post<{ data: Role }>("/roles", data);
},
async updateRole(id: number, data: Partial<RoleCreateRequest>): Promise<{ data: Role }> {
return api.patch<{ data: Role }>(`/roles/${id}`, data);
},
async deleteRole(id: number): Promise<{ success: boolean }> {
return api.delete<{ success: boolean }>(`/roles/${id}`);
},
async updateRolePermissions(id: number, permissionIds: number[]): Promise<{ data: Role }> {
return api.patch<{ data: Role }>(`/roles/${id}/permissions`, { permission_ids: permissionIds });
},
// ── Permissions ──
async listPermissions(params?: { category?: string; search?: string }): Promise<PermissionList> {
return api.get<PermissionList>("/permissions", params as Params);
},
async createPermission(data: PermissionCreateRequest): Promise<{ data: Permission }> {
return api.post<{ data: Permission }>("/permissions", data);
},
async deletePermission(id: number): Promise<{ success: boolean }> {
return api.delete<{ success: boolean }>(`/permissions/${id}`);
},
// ── Role Authority Matrix ──
async getAuthorityMatrix(): Promise<AuthorityMatrixData> {
return api.get<AuthorityMatrixData>("/authority-matrix");
},
async toggleMatrixCell(roleId: number, permissionId: number) {
return api.post<{ role_id: number; permission_id: number; granted: boolean }>(
"/authority-matrix/toggle", { role_id: roleId, permission_id: permissionId },
);
},
// ── User-Role Assignment ──
async listUsersWithRoles(params?: { search?: string; role_id?: number; page?: number; size?: number }): Promise<PaginatedUsers> {
return api.get<PaginatedUsers>("/users/with-roles", params as Params);
},
async getUserRoles(userId: number): Promise<UserRoleDetail> {
return api.get<UserRoleDetail>(`/users/${userId}/roles`);
},
async updateUserRoles(userId: number, roleIds: number[]): Promise<{ success: boolean }> {
return api.patch<{ success: boolean }>(`/users/${userId}/roles`, { role_ids: roleIds });
},
async toggleUserRole(userId: number, roleId: number) {
return api.post<{ user_id: number; role_id: number; assigned: boolean }>(
`/users/${userId}/roles/toggle`, { role_id: roleId },
);
},
// ── User Authority Matrix ──
async getUserAuthorityMatrix(): Promise<UserAuthorityMatrixData> {
return api.get<UserAuthorityMatrixData>("/user-authority-matrix");
},
};

View File

@@ -0,0 +1,17 @@
import { api } from "@/lib/api-client";
import type { PendingScore } from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const scoreReleaseService = {
getPending: (params?: { page?: number; limit?: number }) =>
api.get<{ data: PendingScore[]; pagination: { total: number; page: number } }>("/scores/pending", params as Record<string, string | number | boolean | undefined>),
release: (attemptId: number) =>
api.post<ApiSuccessResponse>(`/scores/${attemptId}/release`),
reject: (attemptId: number, reason: string) =>
api.post<ApiSuccessResponse>(`/scores/${attemptId}/reject`, { reason }),
bulkRelease: (attemptIds: number[]) =>
api.post<ApiSuccessResponse>("/scores/bulk-release", { attempt_ids: attemptIds }),
};

View File

@@ -0,0 +1,31 @@
import { api } from "@/lib/api-client";
import type {
RegisterRequest,
RegisterResponse,
VerifyEmailRequest,
ResendOtpRequest,
CheckEmailResponse,
GoalsResponse,
OnboardingData,
} from "@/types";
import type { ApiSuccessResponse } from "@/types";
export const signupService = {
register: (data: RegisterRequest) =>
api.post<RegisterResponse>("/auth/register", data),
verifyEmail: (data: VerifyEmailRequest) =>
api.post<ApiSuccessResponse>("/auth/verify-email", data),
resendOtp: (data: ResendOtpRequest) =>
api.post<ApiSuccessResponse>("/auth/resend-otp", data),
checkEmail: (email: string) =>
api.post<CheckEmailResponse>("/auth/check-email", { email }),
getGoals: () =>
api.get<GoalsResponse>("/onboarding/goals"),
completeOnboarding: (data: OnboardingData) =>
api.post<ApiSuccessResponse>("/onboarding/complete", data),
};

View File

@@ -0,0 +1,20 @@
import { api } from "@/lib/api-client";
import type { ExamSession, ExamStat, StatisticalData } from "@/types";
export const statsService = {
async getSessions(params?: Record<string, string | number | boolean | undefined>): Promise<ExamSession[]> {
return api.get<ExamSession[]>("/sessions", params);
},
async getStats(params?: Record<string, string | number | boolean | undefined>): Promise<ExamStat[]> {
return api.get<ExamStat[]>("/stats", params);
},
async getStatistical(params?: { entity_id?: number; start_date?: string; end_date?: string }): Promise<StatisticalData> {
return api.get<StatisticalData>("/statistical", params as Record<string, string | number | boolean | undefined>);
},
async getPerformance(params?: { entity_id?: number; search?: string }): Promise<unknown[]> {
return api.get<unknown[]>("/stats/performance", params as Record<string, string | number | boolean | undefined>);
},
};

View File

@@ -0,0 +1,11 @@
import { api } from "@/lib/api-client";
export const storageService = {
async upload(formData: FormData): Promise<{ url: string; filename: string }> {
return api.upload("/storage/upload", formData);
},
async getUrl(filename: string): Promise<{ url: string }> {
return api.get("/storage/url", { filename });
},
};

View File

@@ -0,0 +1,33 @@
import { api } from "@/lib/api-client";
import type { StudentLeaveRequest, StudentLeaveType, StudentLeaveCreateRequest } from "@/types/student-leave";
import type { PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const studentLeaveService = {
async listLeaves(params?: PaginationParams): Promise<PaginatedResponse<StudentLeaveRequest>> {
return api.get<PaginatedResponse<StudentLeaveRequest>>("/student-leaves", params as Record<string, string | number | boolean | undefined>);
},
async createLeave(data: StudentLeaveCreateRequest): Promise<StudentLeaveRequest> {
return api.post<StudentLeaveRequest>("/student-leaves", data);
},
async updateLeave(id: number, data: Partial<StudentLeaveCreateRequest>): Promise<StudentLeaveRequest> {
return api.patch<StudentLeaveRequest>(`/student-leaves/${id}`, data);
},
async approveLeave(id: number): Promise<StudentLeaveRequest> {
return api.post<StudentLeaveRequest>(`/student-leaves/${id}/approve`);
},
async rejectLeave(id: number): Promise<StudentLeaveRequest> {
return api.post<StudentLeaveRequest>(`/student-leaves/${id}/reject`);
},
async listLeaveTypes(params?: PaginationParams): Promise<PaginatedResponse<StudentLeaveType>> {
return api.get<PaginatedResponse<StudentLeaveType>>("/student-leave-types", params as Record<string, string | number | boolean | undefined>);
},
async createLeaveType(data: Pick<StudentLeaveType, "name">): Promise<StudentLeaveType> {
return api.post<StudentLeaveType>("/student-leave-types", data);
},
};

View File

@@ -0,0 +1,9 @@
import { api } from "@/lib/api-client";
import type { StudentProgression } from "@/types/student-progress";
import type { PaginatedResponse, PaginationParams } from "@/types";
export const studentProgressService = {
async listProgress(params?: PaginationParams): Promise<PaginatedResponse<StudentProgression>> {
return api.get<PaginatedResponse<StudentProgression>>("/student-progress", params as Record<string, string | number | boolean | undefined>);
},
};

View File

@@ -0,0 +1,36 @@
import { api } from "@/lib/api-client";
import type { Package, Payment, Discount, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export const subscriptionsService = {
async listPackages(): Promise<Package[]> {
return api.get<Package[]>("/packages");
},
async listPayments(params?: PaginationParams): Promise<PaginatedResponse<Payment>> {
return api.get<PaginatedResponse<Payment>>("/payments", params as Record<string, string | number | boolean | undefined>);
},
async createStripeCheckout(data: { package_id: number; discount_code?: string }): Promise<{ checkout_url: string }> {
return api.post("/stripe/checkout", data);
},
async createPaypalCheckout(data: { package_id: number; discount_code?: string }): Promise<{ approval_url: string }> {
return api.post("/paypal/checkout", data);
},
async createPaymobCheckout(data: { package_id: number; discount_code?: string }): Promise<{ payment_url: string }> {
return api.post("/paymob/checkout", data);
},
async listDiscounts(params?: PaginationParams): Promise<PaginatedResponse<Discount>> {
return api.get<PaginatedResponse<Discount>>("/discounts", params as Record<string, string | number | boolean | undefined>);
},
async createDiscount(data: Partial<Discount>): Promise<Discount> {
return api.post<Discount>("/discounts", data);
},
async deleteDiscount(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/discounts/${id}`);
},
};

View File

@@ -0,0 +1,73 @@
import { api } from "@/lib/api-client";
import type { Subject, Domain, Topic, LearningObjective, TaxonomyTree, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
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 ?? [];
},
async getSubject(id: number): Promise<Subject> {
const res = await api.get<{ data: Subject } | Subject>(`/subjects/${id}`);
return (res as { data: Subject }).data ?? res;
},
async createSubject(data: Partial<Subject>): Promise<Subject> {
return api.post<Subject>("/subjects", data);
},
async updateSubject(id: number, data: Partial<Subject>): Promise<Subject> {
return api.patch<Subject>(`/subjects/${id}`, data);
},
async deleteSubject(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/subjects/${id}`);
},
async getTaxonomyTree(subjectId: number): Promise<TaxonomyTree> {
const res = await api.get<{ data: TaxonomyTree } | TaxonomyTree>(`/subjects/${subjectId}/taxonomy`);
return (res as { data: TaxonomyTree }).data ?? res;
},
async importTaxonomy(subjectId: number, data: FormData): Promise<ApiSuccessResponse> {
return api.upload<ApiSuccessResponse>(`/subjects/${subjectId}/taxonomy/import`, data);
},
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 ?? [];
},
async createDomain(data: Partial<Domain>): Promise<Domain> {
return api.post<Domain>("/domains", data);
},
async updateDomain(id: number, data: Partial<Domain>): Promise<Domain> {
return api.patch<Domain>(`/domains/${id}`, data);
},
async deleteDomain(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/domains/${id}`);
},
async aiSuggestTopics(domainId: number): Promise<{ suggestions: Partial<Topic>[] }> {
return api.post(`/domains/${domainId}/ai-suggest`);
},
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 ?? [];
},
async createTopic(data: Partial<Topic>): Promise<Topic> {
return api.post<Topic>("/topics", data);
},
async updateTopic(id: number, data: Partial<Topic>): Promise<Topic> {
return api.patch<Topic>(`/topics/${id}`, data);
},
async deleteTopic(id: number): Promise<ApiSuccessResponse> {
return api.delete<ApiSuccessResponse>(`/topics/${id}`);
},
};

View File

@@ -0,0 +1,27 @@
import { api } from "@/lib/api-client";
import type { Ticket, TicketCreateRequest, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export interface TicketListParams extends PaginationParams {
status?: string;
type?: string;
assignee_id?: number;
source?: string;
}
export const ticketsService = {
async list(params?: TicketListParams): Promise<PaginatedResponse<Ticket>> {
return api.get<PaginatedResponse<Ticket>>("/tickets", params as Record<string, string | number | boolean | undefined>);
},
async create(data: TicketCreateRequest): Promise<Ticket> {
return api.post<Ticket>("/tickets", data);
},
async update(id: number, data: Partial<Ticket>): Promise<Ticket> {
return api.patch<Ticket>(`/tickets/${id}`, data);
},
async getAssignedToUser(): Promise<Ticket[]> {
return api.get<Ticket[]>("/tickets/assignedToUser");
},
};

View File

@@ -0,0 +1,16 @@
import { api } from "@/lib/api-client";
import type { Training, TrainingTip, Walkthrough } from "@/types";
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>);
},
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);
},
};

View File

@@ -0,0 +1,59 @@
import { api, API_BASE_URL } from "@/lib/api-client";
import { asPaginated, asRecordData } from "@/lib/odoo-api";
import type { User, PaginatedResponse, PaginationParams, ApiSuccessResponse } from "@/types";
export interface UserListParams extends PaginationParams {
type?: string;
entity_id?: number;
}
function mapPortalUser(raw: Record<string, unknown>): User {
return {
id: raw.id as number,
name: String(raw.name || ""),
email: String(raw.email ?? raw.login ?? ""),
login: String(raw.login ?? raw.email ?? ""),
user_type: (raw.user_type as User["user_type"]) || "student",
is_verified: Boolean(raw.is_verified ?? true),
entities: Array.isArray(raw.entities) ? (raw.entities as User["entities"]) : [],
classrooms: Array.isArray(raw.classrooms) ? (raw.classrooms as string[]) : [],
phone: raw.phone as string | undefined,
gender: raw.gender as string | undefined,
student_id: raw.student_id as string | undefined,
};
}
export const usersService = {
async list(params: UserListParams): Promise<PaginatedResponse<User>> {
const raw = await api.get<unknown>("/users/list", params as Record<string, string | number | boolean | undefined>);
const p = asPaginated<Record<string, unknown>>(raw);
return { ...p, items: p.items.map(mapPortalUser) };
},
async getById(id: number): Promise<User> {
const raw = await api.get<unknown>(`/users/${id}`);
return mapPortalUser(asRecordData<Record<string, unknown>>(raw));
},
async update(id: number, data: Partial<User>): Promise<User> {
const raw = await api.patch<unknown>(`/users/update`, { id, ...data });
return mapPortalUser(asRecordData<Record<string, unknown>>(raw));
},
async create(data: Partial<User>): Promise<User> {
const raw = await api.post<unknown>("/users/create", data);
return mapPortalUser(asRecordData<Record<string, unknown>>(raw));
},
async createBatch(data: { users: Partial<User>[] }): Promise<ApiSuccessResponse> {
return api.post<ApiSuccessResponse>("/batch_users", data);
},
async exportList(params: UserListParams): Promise<Blob> {
const res = await fetch(`${API_BASE_URL}/users/export?${new URLSearchParams(params as Record<string, string>)}`, {
headers: { Authorization: `Bearer ${localStorage.getItem("encoach_token") ?? ""}` },
});
if (!res.ok) throw new Error(`Export failed: ${res.status} ${res.statusText}`);
return res.blob();
},
};

View File

@@ -0,0 +1,6 @@
import { api } from "@/lib/api-client";
import type { VerificationPayload } from "@/types";
export const verificationService = {
verify: (hash: string) => api.get<VerificationPayload>(`/verify/${encodeURIComponent(hash)}`),
};