1337 lines
45 KiB
TypeScript
1337 lines
45 KiB
TypeScript
// Course Response structure
|
|
export interface CourseResponse {
|
|
id: number;
|
|
category_id: number;
|
|
title: {
|
|
en: string;
|
|
th: string;
|
|
};
|
|
slug: string;
|
|
description: {
|
|
en: string;
|
|
th: string;
|
|
};
|
|
thumbnail_url: string | null;
|
|
price: string;
|
|
is_free: boolean;
|
|
have_certificate: boolean;
|
|
status: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED';
|
|
approved_by: number | null;
|
|
approved_at: string | null;
|
|
rejection_reason: string | null;
|
|
created_at: string;
|
|
created_by: number;
|
|
updated_at: string;
|
|
updated_by: number | null;
|
|
}
|
|
|
|
export interface ApiResponse<T> {
|
|
code: number;
|
|
message: string;
|
|
data: T;
|
|
}
|
|
|
|
export interface CoursesListResponse {
|
|
code: number;
|
|
message: string;
|
|
data: CourseResponse[];
|
|
total: number;
|
|
}
|
|
|
|
export interface CourseInstructorResponse {
|
|
id: number;
|
|
course_id: number;
|
|
user_id: number;
|
|
is_primary: boolean;
|
|
joined_at: string;
|
|
user: {
|
|
id: number;
|
|
username: string;
|
|
email: string;
|
|
role_id: number;
|
|
email_verified_at: string | null;
|
|
is_deactivated: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
avatar_url?: string;
|
|
};
|
|
}
|
|
|
|
export interface InstructorsListResponse {
|
|
code: number;
|
|
message: string;
|
|
data: CourseInstructorResponse[];
|
|
}
|
|
|
|
// Helper function to get auth token from cookie
|
|
const getAuthToken = (): string => {
|
|
const tokenCookie = useCookie('token');
|
|
return tokenCookie.value || '';
|
|
};
|
|
|
|
// Helper function for making authenticated requests with auto refresh
|
|
const authRequest = async <T>(
|
|
url: string,
|
|
options: {
|
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
body?: any;
|
|
} = {}
|
|
): Promise<T> => {
|
|
const config = useRuntimeConfig();
|
|
|
|
const makeRequest = async (token: string) => {
|
|
return await $fetch<T>(url, {
|
|
baseURL: config.public.apiBaseUrl as string,
|
|
method: options.method || 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`
|
|
},
|
|
body: options.body
|
|
});
|
|
};
|
|
|
|
try {
|
|
const token = getAuthToken();
|
|
return await makeRequest(token);
|
|
} catch (error: any) {
|
|
// If 401, try to refresh token
|
|
if (error.response?.status === 401) {
|
|
const { handleUnauthorized } = await import('~/utils/authFetch');
|
|
const newToken = await handleUnauthorized();
|
|
if (newToken) {
|
|
return await makeRequest(newToken);
|
|
}
|
|
// Redirect to login
|
|
navigateTo('/login');
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Mock courses data
|
|
const MOCK_COURSES: CourseResponse[] = [
|
|
{
|
|
id: 1,
|
|
category_id: 1,
|
|
title: { en: 'JavaScript Fundamentals', th: 'พื้นฐาน JavaScript' },
|
|
slug: 'javascript-fundamentals',
|
|
description: {
|
|
en: 'Learn JavaScript fundamentals from scratch',
|
|
th: 'เรียนรู้พื้นฐาน JavaScript ตั้งแต่เริ่มต้น'
|
|
},
|
|
thumbnail_url: null,
|
|
price: '0',
|
|
is_free: true,
|
|
have_certificate: true,
|
|
status: 'APPROVED',
|
|
approved_by: 1,
|
|
approved_at: '2024-01-15T00:00:00Z',
|
|
rejection_reason: null,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
created_by: 2,
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
updated_by: null
|
|
},
|
|
{
|
|
id: 2,
|
|
category_id: 2,
|
|
title: { en: 'React for Beginners', th: 'React สำหรับผู้เริ่มต้น' },
|
|
slug: 'react-for-beginners',
|
|
description: {
|
|
en: 'Build modern web apps with React',
|
|
th: 'สร้างเว็บแอปพลิเคชันด้วย React'
|
|
},
|
|
thumbnail_url: null,
|
|
price: '990',
|
|
is_free: false,
|
|
have_certificate: true,
|
|
status: 'PENDING',
|
|
approved_by: null,
|
|
approved_at: null,
|
|
rejection_reason: null,
|
|
created_at: '2024-02-01T00:00:00Z',
|
|
created_by: 2,
|
|
updated_at: '2024-02-01T00:00:00Z',
|
|
updated_by: null
|
|
},
|
|
{
|
|
id: 3,
|
|
category_id: 1,
|
|
title: { en: 'TypeScript Masterclass', th: 'TypeScript ขั้นสูง' },
|
|
slug: 'typescript-masterclass',
|
|
description: {
|
|
en: 'Master TypeScript for better JavaScript development',
|
|
th: 'เรียนรู้ TypeScript เพื่อพัฒนา JavaScript ได้ดียิ่งขึ้น'
|
|
},
|
|
thumbnail_url: null,
|
|
price: '1290',
|
|
is_free: false,
|
|
have_certificate: true,
|
|
status: 'DRAFT',
|
|
approved_by: null,
|
|
approved_at: null,
|
|
rejection_reason: null,
|
|
created_at: '2024-02-15T00:00:00Z',
|
|
created_by: 2,
|
|
updated_at: '2024-02-15T00:00:00Z',
|
|
updated_by: null
|
|
}
|
|
];
|
|
|
|
export const instructorService = {
|
|
async getCourses(): Promise<CourseResponse[]> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return MOCK_COURSES;
|
|
}
|
|
|
|
const response = await authRequest<CoursesListResponse>('/api/instructors/courses');
|
|
return response.data;
|
|
},
|
|
|
|
async createCourse(data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 201,
|
|
message: 'Course created successfully (Mock)',
|
|
data: {
|
|
...MOCK_COURSES[0],
|
|
id: Date.now(),
|
|
...data,
|
|
price: String(data.price), // Convert number to string to match CourseResponse type
|
|
status: 'DRAFT',
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
} as CourseResponse
|
|
};
|
|
}
|
|
|
|
// Clean data - remove empty thumbnail_url
|
|
|
|
const cleanedData = { ...data };
|
|
if (!cleanedData.thumbnail_url) {
|
|
delete cleanedData.thumbnail_url;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('data', JSON.stringify(cleanedData));
|
|
|
|
return await authRequest<ApiResponse<CourseResponse>>('/api/instructors/courses', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
},
|
|
|
|
async getCourseById(courseId: number): Promise<CourseDetailResponse> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return MOCK_COURSE_DETAIL;
|
|
}
|
|
|
|
const response = await authRequest<{ code: number; data: CourseDetailResponse }>(`/api/instructors/courses/${courseId}`);
|
|
return response.data;
|
|
},
|
|
|
|
async updateCourse(courseId: number, data: CreateCourseRequest): Promise<ApiResponse<CourseResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Course updated successfully (Mock)',
|
|
data: {
|
|
...MOCK_COURSES[0],
|
|
id: courseId,
|
|
...data,
|
|
price: String(data.price)
|
|
} as CourseResponse
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<CourseResponse>>(`/api/instructors/courses/${courseId}`, {
|
|
method: 'PUT',
|
|
body: { data }
|
|
});
|
|
},
|
|
|
|
async uploadCourseThumbnail(courseId: number, file: File): Promise<ApiResponse<{ thumbnail_url: string }>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Thumbnail uploaded successfully (Mock)',
|
|
data: { thumbnail_url: URL.createObjectURL(file) }
|
|
};
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
return await authRequest<ApiResponse<{ thumbnail_url: string }>>(
|
|
`/api/instructors/courses/${courseId}/thumbnail`,
|
|
{ method: 'POST', body: formData }
|
|
);
|
|
},
|
|
|
|
async getCourseInstructors(courseId: number): Promise<CourseInstructorResponse[]> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return [
|
|
{
|
|
id: 1,
|
|
course_id: courseId,
|
|
user_id: 1,
|
|
is_primary: true,
|
|
joined_at: new Date().toISOString(),
|
|
user: {
|
|
id: 1,
|
|
username: 'instructor',
|
|
email: 'instructor@elearning.local',
|
|
role_id: 2,
|
|
email_verified_at: new Date().toISOString(),
|
|
is_deactivated: false,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
const response = await authRequest<InstructorsListResponse>(
|
|
`/api/instructors/courses/listinstructor/${courseId}`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
async addInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Instructor added successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/add-instructor/${courseId}/${userId}`,
|
|
{ method: 'POST' }
|
|
);
|
|
},
|
|
|
|
async removeInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Instructor removed successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/remove-instructor/${courseId}/${userId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
},
|
|
|
|
async setPrimaryInstructor(courseId: number, userId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Primary instructor updated successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/set-primary-instructor/${courseId}/${userId}`,
|
|
{ method: 'PUT' }
|
|
);
|
|
},
|
|
|
|
async deleteCourse(courseId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Course deleted successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(`/api/instructors/courses/${courseId}`, { method: 'DELETE' });
|
|
},
|
|
|
|
async sendForReview(courseId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Sent for review successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(`/api/instructors/courses/send-review/${courseId}`, { method: 'POST' });
|
|
},
|
|
|
|
async getChapters(courseId: number): Promise<ChapterResponse[]> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return MOCK_COURSE_DETAIL.chapters;
|
|
}
|
|
|
|
// Get chapters from course detail endpoint
|
|
const response = await authRequest<{ code: number; data: { chapters: ChapterResponse[] } }>(
|
|
`/api/instructors/courses/${courseId}`
|
|
);
|
|
return response.data.chapters || [];
|
|
},
|
|
|
|
async createChapter(courseId: number, data: CreateChapterRequest): Promise<ApiResponse<ChapterResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 201,
|
|
message: 'Chapter created successfully (Mock)',
|
|
data: {
|
|
...MOCK_COURSE_DETAIL.chapters[0],
|
|
id: Date.now(),
|
|
...data,
|
|
lessons: []
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<ChapterResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters`,
|
|
{ method: 'POST', body: data }
|
|
);
|
|
},
|
|
|
|
async updateChapter(courseId: number, chapterId: number, data: CreateChapterRequest): Promise<ApiResponse<ChapterResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Chapter updated successfully (Mock)',
|
|
data: {
|
|
...MOCK_COURSE_DETAIL.chapters[0],
|
|
id: chapterId,
|
|
...data
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<ChapterResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
|
|
{ method: 'PUT', body: data }
|
|
);
|
|
},
|
|
|
|
async deleteChapter(courseId: number, chapterId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Chapter deleted successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
},
|
|
|
|
async reorderChapter(courseId: number, chapterId: number, sortOrder: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
return {
|
|
code: 200,
|
|
message: 'Chapter reordered successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder`,
|
|
{ method: 'PUT', body: { sort_order: sortOrder } }
|
|
);
|
|
},
|
|
|
|
// Lesson CRUD
|
|
async getLesson(courseId: number, chapterId: number, lessonId: number): Promise<LessonDetailResponse> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonDetailResponse;
|
|
}
|
|
|
|
const response = await authRequest<{ code: number; data: LessonDetailResponse }>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
async createLesson(courseId: number, chapterId: number, data: CreateLessonRequest): Promise<ApiResponse<LessonResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 201,
|
|
message: 'Lesson created successfully (Mock)',
|
|
data: {
|
|
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
|
|
id: Date.now(),
|
|
...data
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<LessonResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons`,
|
|
{ method: 'POST', body: data }
|
|
);
|
|
},
|
|
|
|
async updateLesson(courseId: number, chapterId: number, lessonId: number, data: UpdateLessonRequest): Promise<ApiResponse<LessonResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Lesson updated successfully (Mock)',
|
|
data: {
|
|
...MOCK_COURSE_DETAIL.chapters[0].lessons[0],
|
|
id: lessonId,
|
|
...data
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<LessonResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
|
|
{ method: 'PUT', body: data }
|
|
);
|
|
},
|
|
|
|
async deleteLesson(courseId: number, chapterId: number, lessonId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Lesson deleted successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
},
|
|
|
|
async reorderLesson(courseId: number, chapterId: number, lessonId: number, sortOrder: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
return {
|
|
code: 200,
|
|
message: 'Lesson reordered successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder-lessons`,
|
|
{ method: 'PUT', body: { lesson_id: lessonId, sort_order: sortOrder } }
|
|
);
|
|
},
|
|
|
|
// Question CRUD
|
|
async createQuestion(courseId: number, chapterId: number, lessonId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 201,
|
|
message: 'Question created successfully (Mock)',
|
|
data: {
|
|
id: Date.now(),
|
|
quiz_id: 1,
|
|
question: data.question,
|
|
explanation: data.explanation || null,
|
|
question_type: data.question_type,
|
|
score: data.score || 1,
|
|
sort_order: data.sort_order || 1,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
choices: data.choices.map((c, i) => ({
|
|
id: Date.now() + i,
|
|
question_id: Date.now(),
|
|
text: c.text,
|
|
is_correct: c.is_correct,
|
|
sort_order: c.sort_order || i + 1
|
|
}))
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<QuizQuestionResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions`,
|
|
{ method: 'POST', body: data }
|
|
);
|
|
},
|
|
|
|
async updateQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, data: CreateQuestionRequest): Promise<ApiResponse<QuizQuestionResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Question updated successfully (Mock)',
|
|
data: {
|
|
id: questionId,
|
|
quiz_id: 1,
|
|
question: data.question,
|
|
explanation: data.explanation || null,
|
|
question_type: data.question_type,
|
|
score: data.score || 1,
|
|
sort_order: data.sort_order || 1,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
choices: data.choices.map((c, i) => ({
|
|
id: Date.now() + i,
|
|
question_id: questionId,
|
|
text: c.text,
|
|
is_correct: c.is_correct,
|
|
sort_order: c.sort_order || i + 1
|
|
}))
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<QuizQuestionResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
|
{ method: 'PUT', body: data }
|
|
);
|
|
},
|
|
|
|
async deleteQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Question deleted successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
},
|
|
|
|
async reorderQuestion(courseId: number, chapterId: number, lessonId: number, questionId: number, sortOrder: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
return {
|
|
code: 200,
|
|
message: 'Question reordered successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/questions/${questionId}/reorder`,
|
|
{ method: 'PUT', body: { sort_order: sortOrder } }
|
|
);
|
|
},
|
|
|
|
// Quiz Settings
|
|
async updateQuizSettings(courseId: number, chapterId: number, lessonId: number, data: UpdateQuizSettingsRequest): Promise<ApiResponse<QuizResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Quiz settings updated successfully (Mock)',
|
|
data: {
|
|
id: 1,
|
|
lesson_id: lessonId,
|
|
title: data.title,
|
|
description: data.description,
|
|
passing_score: data.passing_score,
|
|
time_limit: data.time_limit,
|
|
shuffle_questions: data.shuffle_questions,
|
|
shuffle_choices: data.shuffle_choices,
|
|
show_answers_after_completion: data.show_answers_after_completion
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<QuizResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/quiz`,
|
|
{ method: 'PUT', body: data }
|
|
);
|
|
},
|
|
|
|
// Video Upload
|
|
async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise<ApiResponse<LessonResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
return {
|
|
code: 200,
|
|
message: 'Video uploaded successfully (Mock)',
|
|
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
|
|
};
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('video', video);
|
|
if (attachments) {
|
|
attachments.forEach(file => {
|
|
formData.append('attachments', file);
|
|
});
|
|
}
|
|
|
|
return await authRequest<ApiResponse<LessonResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
|
{ method: 'POST', body: formData }
|
|
);
|
|
},
|
|
|
|
async updateLessonVideo(courseId: number, chapterId: number, lessonId: number, video?: File, attachments?: File[]): Promise<ApiResponse<LessonResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
return {
|
|
code: 200,
|
|
message: 'Video updated successfully (Mock)',
|
|
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
|
|
};
|
|
}
|
|
|
|
const formData = new FormData();
|
|
if (video) {
|
|
formData.append('video', video);
|
|
}
|
|
if (attachments) {
|
|
attachments.forEach(file => {
|
|
formData.append('attachments', file);
|
|
});
|
|
}
|
|
|
|
return await authRequest<ApiResponse<LessonResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`,
|
|
{ method: 'PUT', body: formData }
|
|
);
|
|
},
|
|
|
|
// Attachments
|
|
async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise<ApiResponse<LessonResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Attachments added successfully (Mock)',
|
|
data: MOCK_COURSE_DETAIL.chapters[0].lessons[0] as LessonResponse
|
|
};
|
|
}
|
|
|
|
const formData = new FormData();
|
|
files.forEach(file => {
|
|
formData.append('attachment', file);
|
|
});
|
|
|
|
return await authRequest<ApiResponse<LessonResponse>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments`,
|
|
{ method: 'POST', body: formData }
|
|
);
|
|
},
|
|
|
|
async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Attachment deleted successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
},
|
|
|
|
// Announcements
|
|
async getAnnouncements(courseId: number): Promise<AnnouncementResponse[]> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return MOCK_ANNOUNCEMENTS;
|
|
}
|
|
|
|
const response = await authRequest<AnnouncementsListResponse>(
|
|
`/api/instructors/courses/${courseId}/announcements`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
async createAnnouncement(courseId: number, data: CreateAnnouncementRequest, files?: File[]): Promise<ApiResponse<AnnouncementResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 201,
|
|
message: 'Announcement created successfully (Mock)',
|
|
data: {
|
|
id: Date.now(),
|
|
title: data.title,
|
|
content: data.content,
|
|
status: data.status || 'DRAFT',
|
|
is_pinned: data.is_pinned || false,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
attachments: []
|
|
}
|
|
};
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('data', JSON.stringify(data));
|
|
|
|
if (files && files.length > 0) {
|
|
files.forEach(file => {
|
|
formData.append('files', file);
|
|
});
|
|
}
|
|
|
|
return await authRequest<ApiResponse<AnnouncementResponse>>(
|
|
`/api/instructors/courses/${courseId}/announcements`,
|
|
{ method: 'POST', body: formData }
|
|
);
|
|
},
|
|
|
|
async updateAnnouncement(courseId: number, announcementId: number, data: CreateAnnouncementRequest): Promise<ApiResponse<AnnouncementResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Announcement updated successfully (Mock)',
|
|
data: {
|
|
id: announcementId,
|
|
title: data.title,
|
|
content: data.content,
|
|
status: data.status || 'DRAFT',
|
|
is_pinned: data.is_pinned || false,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
attachments: []
|
|
}
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<AnnouncementResponse>>(
|
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
|
|
{ method: 'PUT', body: data }
|
|
);
|
|
},
|
|
|
|
async deleteAnnouncement(courseId: number, announcementId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Announcement deleted successfully (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
},
|
|
|
|
async uploadAnnouncementAttachment(courseId: number, announcementId: number, file: File): Promise<ApiResponse<AnnouncementResponse>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Upload attachment success (Mock)',
|
|
data: MOCK_ANNOUNCEMENTS[0]
|
|
};
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
return await authRequest<ApiResponse<AnnouncementResponse>>(
|
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments`,
|
|
{ method: 'POST', body: formData }
|
|
);
|
|
},
|
|
|
|
async deleteAnnouncementAttachment(courseId: number, announcementId: number, attachmentId: number): Promise<ApiResponse<void>> {
|
|
const config = useRuntimeConfig();
|
|
const useMockData = config.public.useMockData as boolean;
|
|
|
|
if (useMockData) {
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
return {
|
|
code: 200,
|
|
message: 'Delete attachment success (Mock)',
|
|
data: undefined
|
|
};
|
|
}
|
|
|
|
return await authRequest<ApiResponse<void>>(
|
|
`/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments/${attachmentId}`,
|
|
{ method: 'DELETE' }
|
|
);
|
|
}
|
|
};
|
|
|
|
// Create course request
|
|
export interface CreateCourseRequest {
|
|
category_id: number;
|
|
title: {
|
|
en: string;
|
|
th: string;
|
|
};
|
|
slug: string;
|
|
description: {
|
|
en: string;
|
|
th: string;
|
|
};
|
|
thumbnail_url?: string | null;
|
|
price: number;
|
|
is_free: boolean;
|
|
have_certificate: boolean;
|
|
}
|
|
|
|
// Course detail with chapters and lessons
|
|
export interface CourseDetailResponse extends CourseResponse {
|
|
chapters: ChapterResponse[];
|
|
}
|
|
|
|
export interface ChapterResponse {
|
|
id: number;
|
|
course_id: number;
|
|
title: { en: string; th: string };
|
|
description: { en: string; th: string };
|
|
sort_order: number;
|
|
is_published: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
lessons: LessonResponse[];
|
|
}
|
|
|
|
export interface LessonResponse {
|
|
id: number;
|
|
chapter_id: number;
|
|
title: { en: string; th: string };
|
|
content: { en: string; th: string } | null;
|
|
type: 'VIDEO' | 'QUIZ' | 'DOCUMENT';
|
|
duration_minutes: number;
|
|
sort_order: number;
|
|
is_sequential: boolean;
|
|
prerequisite_lesson_ids: number[] | null;
|
|
require_pass_quiz: boolean;
|
|
is_published: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
video_url: string | null;
|
|
attachments: AttachmentResponse[];
|
|
quiz: QuizResponse | null;
|
|
}
|
|
|
|
export interface QuizChoiceResponse {
|
|
id: number;
|
|
question_id: number;
|
|
text: { en: string; th: string };
|
|
is_correct: boolean;
|
|
sort_order: number;
|
|
}
|
|
|
|
export interface QuizQuestionResponse {
|
|
id: number;
|
|
quiz_id: number;
|
|
question: { en: string; th: string };
|
|
explanation: { en: string; th: string } | null;
|
|
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
|
|
score: number;
|
|
sort_order: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
choices: QuizChoiceResponse[];
|
|
}
|
|
|
|
export interface QuizResponse {
|
|
id: number;
|
|
lesson_id: number;
|
|
title: { en: string; th: string };
|
|
description: { en: string; th: string };
|
|
passing_score: number;
|
|
time_limit: number;
|
|
shuffle_questions: boolean;
|
|
shuffle_choices: boolean;
|
|
show_answers_after_completion: boolean;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
questions?: QuizQuestionResponse[];
|
|
}
|
|
|
|
export interface UpdateQuizSettingsRequest {
|
|
title: { th: string; en: string };
|
|
description: { th: string; en: string };
|
|
passing_score: number;
|
|
time_limit: number;
|
|
shuffle_questions: boolean;
|
|
shuffle_choices: boolean;
|
|
show_answers_after_completion: boolean;
|
|
}
|
|
|
|
export interface CreateChapterRequest {
|
|
title: { th: string; en: string };
|
|
description: { th: string; en: string };
|
|
sort_order?: number;
|
|
is_published?: boolean;
|
|
}
|
|
|
|
export interface CreateChoiceRequest {
|
|
text: { th: string; en: string };
|
|
is_correct: boolean;
|
|
sort_order?: number;
|
|
}
|
|
|
|
export interface CreateQuestionRequest {
|
|
question: { th: string; en: string };
|
|
explanation?: { th: string; en: string } | null;
|
|
question_type: 'MULTIPLE_CHOICE' | 'TRUE_FALSE';
|
|
score?: number;
|
|
sort_order?: number;
|
|
choices: CreateChoiceRequest[];
|
|
}
|
|
|
|
export interface CreateLessonRequest {
|
|
title: { th: string; en: string };
|
|
content?: { th: string; en: string } | null;
|
|
type: 'VIDEO' | 'QUIZ';
|
|
sort_order?: number;
|
|
}
|
|
|
|
export interface UpdateLessonRequest {
|
|
title: { th: string; en: string };
|
|
content?: { th: string; en: string } | null;
|
|
}
|
|
|
|
export interface AttachmentResponse {
|
|
id: number;
|
|
lesson_id: number;
|
|
file_name: string;
|
|
file_path: string;
|
|
file_size: number;
|
|
mime_type: string;
|
|
description: { th: string; en: string };
|
|
sort_order: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface LessonDetailResponse extends LessonResponse {
|
|
attachments: AttachmentResponse[];
|
|
}
|
|
|
|
// Announcement interfaces
|
|
export interface AnnouncementAttachment {
|
|
id: number;
|
|
file_name: string;
|
|
file_path: string;
|
|
file_size: number;
|
|
mime_type: string;
|
|
}
|
|
|
|
export interface AnnouncementResponse {
|
|
id: number;
|
|
title: {
|
|
th: string;
|
|
en: string;
|
|
};
|
|
content: {
|
|
th: string;
|
|
en: string;
|
|
};
|
|
status: 'DRAFT' | 'PUBLISHED';
|
|
is_pinned: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
attachments: AnnouncementAttachment[];
|
|
}
|
|
|
|
export interface AnnouncementsListResponse {
|
|
code: number;
|
|
message: string;
|
|
data: AnnouncementResponse[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export interface CreateAnnouncementRequest {
|
|
title: {
|
|
th: string;
|
|
en: string;
|
|
};
|
|
content: {
|
|
th: string;
|
|
en: string;
|
|
};
|
|
status?: 'DRAFT' | 'PUBLISHED';
|
|
is_pinned?: boolean;
|
|
}
|
|
|
|
// Mock announcements
|
|
const MOCK_ANNOUNCEMENTS: AnnouncementResponse[] = [
|
|
{
|
|
id: 1,
|
|
title: { th: 'ยินดีต้อนรับสู่คอร์ส', en: 'Welcome to the Course' },
|
|
content: {
|
|
th: 'ยินดีต้อนรับทุกคนสู่คอร์ส JavaScript Fundamentals! เราจะเริ่มเรียนในสัปดาห์หน้า',
|
|
en: 'Welcome everyone to JavaScript Fundamentals! We will start next week'
|
|
},
|
|
status: 'PUBLISHED',
|
|
is_pinned: true,
|
|
created_at: '2024-01-15T10:00:00Z',
|
|
updated_at: '2024-01-15T10:00:00Z',
|
|
attachments: []
|
|
},
|
|
{
|
|
id: 2,
|
|
title: { th: 'อัพเดทตารางเรียน', en: 'Schedule Update' },
|
|
content: {
|
|
th: 'มีการเปลี่ยนแปลงตารางเรียนสำหรับบทที่ 3',
|
|
en: 'There is a schedule change for Chapter 3'
|
|
},
|
|
status: 'PUBLISHED',
|
|
is_pinned: false,
|
|
created_at: '2024-01-20T14:00:00Z',
|
|
updated_at: '2024-01-20T14:00:00Z',
|
|
attachments: []
|
|
}
|
|
];
|
|
|
|
// Mock course detail
|
|
const MOCK_COURSE_DETAIL: CourseDetailResponse = {
|
|
...MOCK_COURSES[0],
|
|
chapters: [
|
|
{
|
|
id: 1,
|
|
course_id: 1,
|
|
title: { en: 'Chapter 1: Getting Started', th: 'บทที่ 1: เริ่มต้น' },
|
|
description: { en: 'Introduction to JavaScript', th: 'แนะนำ JavaScript' },
|
|
sort_order: 1,
|
|
is_published: true,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
lessons: [
|
|
{
|
|
id: 1,
|
|
chapter_id: 1,
|
|
title: { en: 'What is JavaScript', th: 'JavaScript คืออะไร' },
|
|
content: { en: 'Introduction', th: 'แนะนำ' },
|
|
type: 'VIDEO',
|
|
duration_minutes: 15,
|
|
sort_order: 1,
|
|
is_sequential: true,
|
|
prerequisite_lesson_ids: null,
|
|
require_pass_quiz: false,
|
|
is_published: true,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
video_url: null,
|
|
attachments: [],
|
|
quiz: null
|
|
},
|
|
{
|
|
id: 2,
|
|
chapter_id: 1,
|
|
title: { en: 'Variables', th: 'ตัวแปร' },
|
|
content: { en: 'Learn variables', th: 'เรียนรู้ตัวแปร' },
|
|
type: 'VIDEO',
|
|
duration_minutes: 20,
|
|
sort_order: 2,
|
|
is_sequential: true,
|
|
prerequisite_lesson_ids: null,
|
|
require_pass_quiz: false,
|
|
is_published: true,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
video_url: null,
|
|
attachments: [],
|
|
quiz: null
|
|
},
|
|
{
|
|
id: 3,
|
|
chapter_id: 1,
|
|
title: { en: 'Chapter 1 Quiz', th: 'แบบทดสอบบทที่ 1' },
|
|
content: null,
|
|
type: 'QUIZ',
|
|
duration_minutes: 10,
|
|
sort_order: 3,
|
|
is_sequential: true,
|
|
prerequisite_lesson_ids: null,
|
|
require_pass_quiz: true,
|
|
is_published: true,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
video_url: null,
|
|
attachments: [],
|
|
quiz: {
|
|
id: 1,
|
|
lesson_id: 3,
|
|
title: { en: 'Chapter 1 Quiz', th: 'แบบทดสอบบทที่ 1' },
|
|
description: { en: 'Test your knowledge', th: 'ทดสอบความรู้' },
|
|
passing_score: 70,
|
|
time_limit: 10,
|
|
shuffle_questions: true,
|
|
shuffle_choices: true,
|
|
show_answers_after_completion: true
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 2,
|
|
course_id: 1,
|
|
title: { en: 'Chapter 2: Functions', th: 'บทที่ 2: ฟังก์ชัน' },
|
|
description: { en: 'Learn about functions', th: 'เรียนรู้เกี่ยวกับฟังก์ชัน' },
|
|
sort_order: 2,
|
|
is_published: true,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
lessons: [
|
|
{
|
|
id: 4,
|
|
chapter_id: 2,
|
|
title: { en: 'Creating Functions', th: 'การสร้างฟังก์ชัน' },
|
|
content: { en: 'How to create functions', th: 'วิธีสร้างฟังก์ชัน' },
|
|
type: 'VIDEO',
|
|
duration_minutes: 25,
|
|
sort_order: 1,
|
|
is_sequential: true,
|
|
prerequisite_lesson_ids: null,
|
|
require_pass_quiz: false,
|
|
is_published: true,
|
|
created_at: '2024-01-15T00:00:00Z',
|
|
updated_at: '2024-01-15T00:00:00Z',
|
|
video_url: null,
|
|
attachments: [],
|
|
quiz: null
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|