// Interface สำหรับข้อมูลคอร์สเรียน (Public Course Data) export interface Course { id: number title: string | { th: string; en: string } // รองรับ 2 ภาษา slug: string description: string | { th: string; en: string } thumbnail_url: string price: string is_free: boolean original_price?: string have_certificate: boolean status: string // DRAFT, PUBLISHED category_id: number created_at?: string updated_at?: string created_by?: number updated_by?: number approved_at?: string approved_by?: number rejection_reason?: string enrolled?: boolean total_lessons?: number rating?: string lessons?: number | string levelType?: 'neutral' | 'warning' | 'success' // ใช้สำหรับการแสดงผล Badge ระดับความยาก (Frontend Logic) // โครงสร้างบทเรียน (Chapters & Lessons) chapters?: { id: number title: string | { th: string; en: string } lessons: { id: number title: string | { th: string; en: string } duration_minutes: number video_url?: string }[] }[] // ข้อมูลผู้สอนและเจ้าของคอร์ส creator?: { id: number username: string email: string profile: { first_name: string last_name: string avatar_url: string } } instructors?: { user_id: number is_primary: boolean user: { id: number username: string email: string profile: { first_name: string last_name: string avatar_url: string } } }[] } interface CourseResponse { code: number message: string data: Course[] total: number page?: number limit?: number totalPages?: number } interface SingleCourseResponse { code: number message: string data: Course } // Interface สำหรับคอร์สที่ผู้ใช้ลงทะเบียนเรียนแล้ว (My Course) export interface EnrolledCourse { id: number course_id: number course: Course status: 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED' progress_percentage: number enrolled_at: string started_at?: string completed_at?: string last_accessed_at?: string } interface EnrolledCourseResponse { code: number message: string data: EnrolledCourse[] total: number page: number limit: number } // Interface สำหรับการส่งคำตอบแบบทดสอบ (Quiz Submission) export interface QuizAnswerSubmission { question_id: number choice_id: number } export interface QuizSubmitRequest { answers: QuizAnswerSubmission[] } // Interface สำหรับผลลัพธ์การสอบ (Quiz Result) export interface QuizResult { answers_review: { score: number is_correct: boolean correct_choice_id: number selected_choice_id: number question_id: number }[] completed_at: string started_at: string attempt_number: number passing_score: number is_passed: boolean correct_answers: number total_questions: number total_score: number score: number quiz_id: number attempt_id: number } // Interface สำหรับ Certificate export interface Certificate { certificate_id: number course_id: number course_title: { en: string th: string } issued_at: string download_url: string } // ========================================== // Composable: useCourse // หน้าที่: จัดการ Logic ทุกอย่างเกี่ยวกับคอร์สเรียน // - ดึงข้อมูลคอร์ส (Public & Protected) // - ลงทะเบียนเรียน (Enroll) // - ติดตามความคืบหน้าการเรียน (Progress tracking) export const useCourse = () => { const config = useRuntimeConfig() const API_BASE_URL = config.public.apiBase as string const { token } = useAuth() // ใช้ useState เพื่อเก็บรายชื่อคอร์สทั้งหมดใน Memory (สำหรับกรณีดึงทั้งหมด) const coursesState = useState('courses_cache', () => []) const isCoursesLoaded = useState('courses_loaded', () => false) /** * ดึงรายชื่อคอร์สทั้งหมด (Catalog) * รองรับการกรองด้วยหมวดหมู่ และ Pagination * Endpoint: GET /courses */ const fetchCourses = async (params: { category_id?: number; page?: number; limit?: number; random?: boolean; is_recommended?: boolean; forceRefresh?: boolean } = {}) => { const { forceRefresh = false, ...apiParams } = params // ใช้ Cache เฉพาะกรณีดึง "ทั้งหมด" แบบปกติ (ไม่มี params) const isRequestingAll = Object.keys(apiParams).length === 0 if (isRequestingAll && isCoursesLoaded.value && !forceRefresh && coursesState.value.length > 0) { return { success: true, data: coursesState.value, total: coursesState.value.length } } try { // สร้าง Query String const queryParams = new URLSearchParams() if (apiParams.category_id) queryParams.append('category_id', apiParams.category_id.toString()) if (apiParams.page) queryParams.append('page', apiParams.page.toString()) if (apiParams.limit) queryParams.append('limit', apiParams.limit.toString()) if (apiParams.random !== undefined) queryParams.append('random', apiParams.random.toString()) if (apiParams.is_recommended !== undefined) queryParams.append('is_recommended', apiParams.is_recommended.toString()) const queryString = queryParams.toString() const url = `${API_BASE_URL}/courses${queryString ? `?${queryString}` : ''}` const data = await $fetch(url, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) const courses = data.data || [] // เก็บลง State เฉพาะกรณีดึง "ทั้งหมด" if (isRequestingAll) { coursesState.value = courses isCoursesLoaded.value = true } return { success: true, data: courses, total: data.total || 0, page: data.page, limit: data.limit, totalPages: data.totalPages } } catch (err: any) { console.error('Fetch courses failed:', err) // Retry logic logic for 429 if (err.statusCode === 429 || err.status === 429) { await new Promise(resolve => setTimeout(resolve, 1500)) return fetchCourses(params) // Recursive retry } return { success: false, error: err.data?.message || err.message || 'Error fetching courses' } } } /** * ดึงรายละเอียดคอร์สตาม ID * Endpoint: GET /courses/:id */ const fetchCourseById = async (id: number) => { try { const data = await $fetch(`${API_BASE_URL}/courses/${id}`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) if (!data.data) throw new Error('Course not found') return { success: true, data: data.data // ข้อมูลคอร์สตัวเดียว } } catch (err: any) { console.error('Fetch course details failed:', err) return { success: false, error: err.data?.message || err.message || 'Error fetching course details' } } } // ฟังก์ชันลงทะเบียนเรียน (Enroll) // Endpoint: POST /students/courses/:id/enroll const enrollCourse = async (courseId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/enroll`, { method: 'POST', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data, message: data.message } } catch (err: any) { const status = err.statusCode || err.status || err.response?.status const errorData = err.data?.error || err.data // เช็ค Error 409 Conflict หรือ 400 Bad Request (กรณีลงทะเบียนไปแล้ว) // สำหรับกรณีนี้ เราจะไม่ log console.error ให้รกหน้าจอเพราะเป็นเรื่องที่ดักจับได้ if (status === 409 || (status === 400 && errorData?.message?.includes('Already enrolled'))) { return { success: false, error: 'ท่านได้ลงทะเบียนไปแล้ว', code: 409 // treat internally as conflict } } console.error('Enroll course failed:', err) return { success: false, error: errorData?.message || err.message || 'Error enrolling in course', code: errorData?.code || err.data?.code } } } // ฟังก์ชันดึงคอร์สที่ฉันลงทะเบียนเรียน (My Courses) // รองรับ Pagination และการกรอง Status (ENROLLED, IN_PROGRESS, COMPLETED) const fetchEnrolledCourses = async (params: { page?: number; limit?: number; status?: string } = {}) => { try { const queryParams = new URLSearchParams() if (params.page) queryParams.append('page', params.page.toString()) if (params.limit) queryParams.append('limit', params.limit.toString()) if (params.status && params.status !== 'ALL') queryParams.append('status', params.status) const data = await $fetch(`${API_BASE_URL}/students/courses?${queryParams.toString()}`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data || [], total: data.total || 0, page: data.page, limit: data.limit } } catch (err: any) { console.error('Fetch enrolled courses failed:', err) // Retry for 429 if (err.statusCode === 429 || err.status === 429) { await new Promise(resolve => setTimeout(resolve, 1500)); try { const queryParams = new URLSearchParams() if (params.page) queryParams.append('page', params.page.toString()) if (params.limit) queryParams.append('limit', params.limit.toString()) if (params.status && params.status !== 'ALL') queryParams.append('status', params.status) const retryData = await $fetch(`${API_BASE_URL}/students/courses?${queryParams.toString()}`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: retryData.data || [], total: retryData.total || 0, page: retryData.page, limit: retryData.limit } } catch (retryErr) { console.error('Retry fetch enrolled courses failed:', retryErr) } } return { success: false, error: err.data?.message || err.message || 'Error fetching enrolled courses' } } } // ฟังก์ชันดึงข้อมูลสำหรับการเรียน (Chapters, Lessons, Progress) // Endpoint: GET /students/courses/:id/learn const fetchCourseLearningInfo = async (courseId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/learn`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data } } catch (err: any) { console.error('Fetch course learning info failed:', err) // Retry for 429 if (err.statusCode === 429 || err.status === 429) { await new Promise(resolve => setTimeout(resolve, 1500)); try { const retryData = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/learn`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: retryData.data } } catch (retryErr) { console.error('Retry fetch course learning info failed:', retryErr) } } return { success: false, error: err.data?.message || err.message || 'Error fetching course learning info', code: err.data?.code } } } // ฟังก์ชันดึงเนื้อหาบทเรียน (Video, Content) // Endpoint: GET /students/courses/:cid/lessons/:lid const fetchLessonContent = async (courseId: number, lessonId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any; progress?: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data, progress: data.progress } } catch (err: any) { console.error('Fetch lesson content failed:', err) return { success: false, error: err.data?.message || err.message || 'Error fetching lesson content', code: err.data?.code, status: err.status } } } // ฟังก์ชันเช็คสิทธิ์การเข้าถึงบทเรียน (Access Control) // ต้อง Enrolled ก่อนถึงจะเข้าได้ และต้องผ่านเงื่อนไข Prerequisites (ถ้ามี) // Endpoint: GET /students/courses/:cid/lessons/:lid/access-check const checkLessonAccess = async (courseId: number, lessonId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/access-check`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data } } catch (err: any) { console.error('Check lesson access failed:', err) return { success: false, error: err.data?.message || err.message || 'Error checking lesson access', code: err.data?.code, status: err.status } } } // ฟังก์ชันบันทึกเวลาที่ดูวิดีโอ (Video Progress) // Endpoint: POST /students/lessons/:id/progress const saveVideoProgress = async (lessonId: number, progressSeconds: number, durationSeconds: number, keepalive: boolean = false) => { try { const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, { method: 'POST', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}, body: { video_progress_seconds: progressSeconds, video_duration_seconds: durationSeconds }, keepalive: keepalive }) return { success: true, data: data.data } } catch (err: any) { console.error('Save video progress failed:', err) return { success: false, error: err.data?.message || err.message || 'Error saving video progress', code: err.data?.code } } } // ฟังก์ชันดึง Video Progress ปัจจุบันของบทเรียน // Endpoint: GET /students/lessons/:id/progress const fetchVideoProgress = async (lessonId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/lessons/${lessonId}/progress`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data } } catch (err: any) { console.error('Fetch video progress failed:', err) return { success: false, error: err.data?.message || err.message || 'Error fetching video progress', code: err.data?.code, status: err.status } } } // ฟังก์ชันบันทึกว่าเรียนจบบทเรียนแล้ว (Mark Complete) // Endpoint: POST /students/courses/:cid/lessons/:lid/complete const markLessonComplete = async (courseId: number, lessonId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/complete`, { method: 'POST', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data } } catch (err: any) { console.error('Mark lesson complete failed:', err) return { success: false, error: err.data?.message || err.message || 'Error marking lesson as complete', code: err.data?.code, status: err.status } } } // ฟังก์ชันส่งคำตอบ Quiz // Endpoint: POST /students/courses/:cid/lessons/:lid/quiz/submit const submitQuiz = async (courseId: number, lessonId: number, answers: QuizAnswerSubmission[], alreadyPassed: boolean = false) => { try { // NOTE: Backend crashes with 500 if we send extra fields like 'already_passed'. // Reverting to strict body structure. const body = { answers } const data = await $fetch<{ code: number; message: string; data: QuizResult }>(`${API_BASE_URL}/students/courses/${courseId}/lessons/${lessonId}/quiz/submit`, { method: 'POST', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {}, body: body }) return { success: true, data: data.data } } catch (err: any) { console.error('Submit quiz failed:', err) return { success: false, error: err.data?.message || err.message || 'Error submitting quiz', code: err.data?.code, status: err.status } } } // ฟังก์ชันสร้างใบ Certificate (Create/Generate) // Endpoint: POST /certificates/:courseId/generate const generateCertificate = async (courseId: number) => { try { const data = await $fetch<{ code: number; message: string; data: Certificate }>(`${API_BASE_URL}/certificates/${courseId}/generate`, { method: 'POST', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data } } catch (err: any) { console.error('Generate certificate failed:', err) return { success: false, error: err.data?.message || err.message || 'Error generating certificate', code: err.data?.code, status: err.status } } } // ฟังก์ชันดึงใบ Certificate ของคอร์สที่ระบุ (แบบเดี่ยว - ใหม่) // Endpoint: GET /certificates/:courseId const getCertificate = async (courseId: number) => { try { const data = await $fetch<{ code: number; message: string; data: Certificate }>(`${API_BASE_URL}/certificates/${courseId}`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data // Return single certificate object } } catch (err: any) { console.error('Get certificate failed:', err) return { success: false, error: err.data?.message || err.message || 'Error getting certificate', code: err.data?.code, status: err.status } } } // ฟังก์ชันดึงประกาศของคอร์ส (Announcements) // Endpoint: GET /student/courses/:id/announcements (Note: 'student' singular based on recent API update) const fetchCourseAnnouncements = async (courseId: number) => { try { const data = await $fetch<{ code: number; message: string; data: any[] }>(`${API_BASE_URL}/student/courses/${courseId}/announcements`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data } } catch (err: any) { console.error('Fetch course announcements failed:', err) return { success: false, error: err.data?.message || err.message || 'Error fetching announcements', code: err.data?.code, status: err.status } } } // ฟังก์ชันดึงรายการใบ Certificate ทั้งหมดของผู้ใช้ (ใหม่) // Endpoint: GET /certificates const fetchAllCertificates = async () => { try { const data = await $fetch<{ code: number; message: string; data: Certificate[] }>(`${API_BASE_URL}/certificates`, { method: 'GET', headers: token.value ? { Authorization: `Bearer ${token.value}` } : {} }) return { success: true, data: data.data || [] } } catch (err: any) { console.error('Fetch all certificates failed:', err) return { success: false, error: err.data?.message || err.message || 'Error fetching certificates', code: err.data?.code, status: err.status } } } const { locale } = useI18n() /** * Helper: แปลงข้อมูล 2 ภาษาเป็นข้อความตาม locale ปัจจุบัน หรือค่าที่มีอยู่ */ const getLocalizedText = (text: string | { th: string; en: string } | undefined | null) => { if (!text) return '' if (typeof text === 'string') return text // Return based on current locale, fallback to th then en const currentLocale = locale.value as 'th' | 'en' // @ts-ignore return text[currentLocale] || text.th || text.en || '' } return { getLocalizedText, fetchCourses, fetchCourseById, enrollCourse, fetchEnrolledCourses, fetchCourseLearningInfo, fetchLessonContent, checkLessonAccess, saveVideoProgress, fetchVideoProgress, markLessonComplete, submitQuiz, generateCertificate, getCertificate, fetchAllCertificates, fetchCourseAnnouncements } }