// 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 { 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[]; } export interface SearchInstructorResult { id: number; username: string; email: string; profile: { first_name: string; last_name: string; avatar_url: string | null; }; } export interface SearchInstructorsResponse { code: number; message: string; data: SearchInstructorResult[]; } export interface EnrolledStudentResponse { user_id: number; username: string; email: string; first_name: string; last_name: string; avatar_url: string | null; enrolled_at: string; progress_percentage: number; status: 'ENROLLED' | 'COMPLETED' | 'DROPPED'; } export interface EnrolledStudentsListResponse { code: number; message: string; data: EnrolledStudentResponse[]; total: number; page: number; limit: number; } export interface StudentSearchResult { user_id: number; first_name: string; last_name: string; email: string; } export interface StudentSearchResponse { code: number; message: string; data: StudentSearchResult[]; } // Student Detail with Progress interfaces export interface StudentDetailLesson { lesson_id: number; lesson_title: { th: string; en: string }; lesson_type: string; sort_order: number; is_completed: boolean; completed_at: string | null; video_progress_seconds: number; video_duration_seconds: number; video_progress_percentage: number; last_watched_at: string | null; } export interface StudentDetailChapter { chapter_id: number; chapter_title: { th: string; en: string }; sort_order: number; lessons: StudentDetailLesson[]; completed_lessons: number; total_lessons: number; } export interface StudentDetailData { student: { user_id: number; username: string; email: string; first_name: string; last_name: string; avatar_url: string | null; }; enrollment: { status: string; progress_percentage: number; enrolled_at: string; }; chapters: StudentDetailChapter[]; total_completed_lessons: number; total_lessons: number; } export interface StudentDetailResponse { code: number; message: string; data: StudentDetailData; } // 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 ( url: string, options: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; } = {} ): Promise => { const config = useRuntimeConfig(); const makeRequest = async (token: string) => { return await $fetch(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; } }; export const instructorService = { async getCourses(): Promise { const response = await authRequest('/api/instructors/courses'); return response.data; }, async createCourse(data: CreateCourseRequest): Promise> { // 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>('/api/instructors/courses', { method: 'POST', body: formData }); }, async getCourseById(courseId: number): Promise { const response = await authRequest<{ code: number; data: CourseDetailResponse }>(`/api/instructors/courses/${courseId}`); return response.data; }, async updateCourse(courseId: number, data: CreateCourseRequest): Promise> { return await authRequest>(`/api/instructors/courses/${courseId}`, { method: 'PUT', body: { data } }); }, async uploadCourseThumbnail(courseId: number, file: File): Promise> { const formData = new FormData(); formData.append('file', file); return await authRequest>( `/api/instructors/courses/${courseId}/thumbnail`, { method: 'POST', body: formData } ); }, async submitCourseForApproval(courseId: number): Promise> { return await authRequest>( `/api/instructors/courses/send-review/${courseId}`, { method: 'POST' } ); }, async getCourseInstructors(courseId: number): Promise { const response = await authRequest( `/api/instructors/courses/listinstructor/${courseId}` ); return response.data; }, async addInstructor(courseId: number, emailOrUsername: string): Promise> { return await authRequest>( `/api/instructors/courses/add-instructor/${courseId}/${encodeURIComponent(emailOrUsername)}`, { method: 'POST' } ); }, async removeInstructor(courseId: number, userId: number): Promise> { return await authRequest>( `/api/instructors/courses/remove-instructor/${courseId}/${userId}`, { method: 'DELETE' } ); }, async setPrimaryInstructor(courseId: number, userId: number): Promise> { return await authRequest>( `/api/instructors/courses/set-primary-instructor/${courseId}/${userId}`, { method: 'PUT' } ); }, async searchInstructors(query: string, courseId: number): Promise { const response = await authRequest( `/api/instructors/courses/${courseId}/search-instructors?query=${encodeURIComponent(query)}` ); return response.data; }, async deleteCourse(courseId: number): Promise> { return await authRequest>(`/api/instructors/courses/${courseId}`, { method: 'DELETE' }); }, async sendForReview(courseId: number): Promise> { return await authRequest>(`/api/instructors/courses/send-review/${courseId}`, { method: 'POST' }); }, async setCourseDraft(courseId: number): Promise> { return await authRequest>(`/api/instructors/courses/set-draft/${courseId}`, { method: 'POST' }); }, async getEnrolledStudents( courseId: number, page: number = 1, limit: number = 10, search?: string, status?: string ): Promise { let url = `/api/instructors/courses/${courseId}/students?page=${page}&limit=${limit}`; if (search) { url += `&search=${encodeURIComponent(search)}`; } if (status && status !== 'all') { url += `&status=${status}`; } return await authRequest(url); }, async searchStudentsInCourse(courseId: number, query: string, limit: number = 5): Promise { const response = await authRequest( `/api/instructors/courses/${courseId}/students/search?query=${encodeURIComponent(query)}&limit=${limit}` ); return response.data; }, async getStudentDetail(courseId: number, studentId: number): Promise { const response = await authRequest( `/api/instructors/courses/${courseId}/students/${studentId}` ); return response.data; }, async getChapters(courseId: number): Promise { // 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> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters`, { method: 'POST', body: data } ); }, async updateChapter(courseId: number, chapterId: number, data: CreateChapterRequest): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}`, { method: 'PUT', body: data } ); }, async deleteChapter(courseId: number, chapterId: number): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}`, { method: 'DELETE' } ); }, async reorderChapter(courseId: number, chapterId: number, sortOrder: number): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder`, { method: 'PUT', body: { sort_order: sortOrder } } ); }, // Lesson CRUD async getLesson(courseId: number, chapterId: number, lessonId: number): Promise { 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> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons`, { method: 'POST', body: data } ); }, async updateLesson(courseId: number, chapterId: number, lessonId: number, data: UpdateLessonRequest): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`, { method: 'PUT', body: data } ); }, async deleteLesson(courseId: number, chapterId: number, lessonId: number): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}`, { method: 'DELETE' } ); }, async reorderLesson(courseId: number, chapterId: number, lessonId: number, sortOrder: number): Promise> { return await authRequest>( `/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> { return await authRequest>( `/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> { return await authRequest>( `/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> { return await authRequest>( `/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> { return await authRequest>( `/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> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/quiz`, { method: 'PUT', body: data } ); }, // Quiz Scores async getLessonQuizScores( courseId: number, lessonId: number, page: number = 1, limit: number = 10, search?: string, isPassed?: boolean ): Promise { let url = `/api/instructors/courses/${courseId}/lessons/${lessonId}/quiz/scores?page=${page}&limit=${limit}`; if (search) { url += `&search=${encodeURIComponent(search)}`; } if (isPassed !== undefined) { url += `&isPassed=${isPassed}`; } return await authRequest(url); }, async getStudentQuizAttemptDetail( courseId: number, lessonId: number, studentId: number ): Promise { return await authRequest( `/api/instructors/courses/${courseId}/lessons/${lessonId}/quiz/students/${studentId}` ); }, // Video Upload async uploadLessonVideo(courseId: number, chapterId: number, lessonId: number, video: File, attachments?: File[]): Promise> { const formData = new FormData(); formData.append('video', video); if (attachments) { attachments.forEach(file => { formData.append('attachments', file); }); } return await authRequest>( `/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> { const formData = new FormData(); if (video) { formData.append('video', video); } if (attachments) { attachments.forEach(file => { formData.append('attachments', file); }); } return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/video`, { method: 'PUT', body: formData } ); }, async setLessonYoutubeVideo(courseId: number, chapterId: number, lessonId: number, youtubeVideoId: string, videoTitle: string): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/youtube-video`, { method: 'POST', body: { youtube_video_id: youtubeVideoId, video_title: videoTitle } } ); }, // Attachments async addAttachments(courseId: number, chapterId: number, lessonId: number, files: File[]): Promise> { const formData = new FormData(); files.forEach(file => { formData.append('attachment', file); }); return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments`, { method: 'POST', body: formData } ); }, async deleteAttachment(courseId: number, chapterId: number, lessonId: number, attachmentId: number): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/chapters/${chapterId}/lessons/${lessonId}/attachments/${attachmentId}`, { method: 'DELETE' } ); }, // Announcements async getAnnouncements(courseId: number): Promise { const response = await authRequest( `/api/instructors/courses/${courseId}/announcements` ); return response.data; }, async createAnnouncement(courseId: number, data: CreateAnnouncementRequest, files?: File[]): Promise> { 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>( `/api/instructors/courses/${courseId}/announcements`, { method: 'POST', body: formData } ); }, async updateAnnouncement(courseId: number, announcementId: number, data: CreateAnnouncementRequest): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/announcements/${announcementId}`, { method: 'PUT', body: data } ); }, async deleteAnnouncement(courseId: number, announcementId: number): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/announcements/${announcementId}`, { method: 'DELETE' } ); }, async uploadAnnouncementAttachment(courseId: number, announcementId: number, file: File): Promise> { const formData = new FormData(); formData.append('file', file); return await authRequest>( `/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments`, { method: 'POST', body: formData } ); }, async deleteAnnouncementAttachment(courseId: number, announcementId: number, attachmentId: number): Promise> { return await authRequest>( `/api/instructors/courses/${courseId}/announcements/${announcementId}/attachments/${attachmentId}`, { method: 'DELETE' } ); }, async getCourseApprovalHistory(courseId: number): Promise { const response = await authRequest<{ code: number; message: string; data: { approval_history: ApprovalHistory[]; current_status: string; course_title: { en: string; th: string }; course_id: number; } }>(`/api/instructors/courses/${courseId}/approval-history`); return response.data.approval_history; } }; // Approval History Interface export interface ApprovalHistory { id: number; action: string; comment: string | null; created_at: string; submitter: { id: number; username: string; email: string; }; reviewer: { id: number; username: string; email: string; } | null; } // 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; is_skippable: boolean; allow_multiple_attempts: 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; is_skippable: boolean; allow_multiple_attempts: 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; prerequisite_lesson_ids?: number[] | 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; published_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; }; is_pinned?: boolean; status?: 'DRAFT' | 'PUBLISHED'; published_at?: string; } export interface QuizScoreStudentResponse { user_id: number; username: string; email: string; first_name: string; last_name: string; avatar_url: string | null; latest_attempt: { completed_at: string; attempt_number: number; is_passed: boolean; total_score: number; score: number; id: number; } | null; best_score: number; total_attempts: number; is_passed: boolean; } export interface QuizScoresData { students: QuizScoreStudentResponse[]; total_score: number; passing_score: number; quiz_title: { th: string; en: string; }; quiz_id: number; lesson_title: { th: string; en: string; }; lesson_id: number; } export interface QuizScoresResponse { code: number; message: string; data: QuizScoresData; total: number; page: number; limit: number; } export interface QuizAnswerReview { question_id: number; question_text: { th: string; en: string; }; sort_order: number; selected_choice_id: number; selected_choice_text: { th: string; en: string; }; is_correct: boolean; score: number; question_score: number; } export interface QuizAttemptDetailData { attempt_id: number; quiz_id: number; student: { user_id: number; username: string; email: string; first_name: string; last_name: string; }; score: number; total_score: number; total_questions: number; correct_answers: number; is_passed: boolean; passing_score: number; attempt_number: number; started_at: string; completed_at: string; answers_review: QuizAnswerReview[]; } export interface QuizAttemptDetailResponse { code: number; message: string; data: QuizAttemptDetailData; }